Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

f8 for The Rust Programming Language

Crates.io Docs.rs LoC

One byte. Exact quantization. No allocator. No std.

f8 is an unsigned normalized 8-bit number, representing bits / 255 in [0, 1]. It is suitable for compact colors, coverage, probabilities, masks, weights, and normalized device data. It is not IEEE FP8, E4M3, or E5M2.

[dependencies]
f8 = "0.2"
use f8::f8;

const HALF: f8 = f8::from_f32(0.5);
assert_eq!(HALF.to_bits(), 128);
assert_eq!(HALF.to_f32(), 128.0 / 255.0);
assert_eq!(HALF + HALF, f8::ONE);
assert_eq!((HALF * HALF).to_bits(), 64);

let values = [HALF, f8::ONE];
assert_eq!(values.iter().sum::<f8>(), f8::ONE);
assert_eq!(values.iter().product::<f8>(), HALF);

Numerical Contract

  • Raw encoding b denotes the exact real number b / 255. All 256 encodings are valid; zero and one are exact endpoints.
  • from_f32(x) clamps to [0, 1], then rounds the exact product 255*x to nearest, ties to even. All NaNs become zero; infinities saturate.
  • to_f32() returns correctly rounded b / 255. Every byte round-trips.
  • Quantization error is at most 1/510 against the clamped input, before the final f32 decoding roundoff. This is a uniform grid, not relative precision.
  • Addition/subtraction saturate instead of wrapping or panicking. Multiplication and division saturate and round to nearest, ties to even.
  • 0 / 0 = 0; positive / 0 = 1. No arithmetic operation panics, including in debug builds. Arithmetic is not IEEE floating-point arithmetic.

The rounding rule is deliberate. (x * 255.0_f32).round() is not equivalent: it uses different tie-breaking and can round the multiplication onto a false midpoint. The scalar and accelerated encoders avoid both problems using integer significand arithmetic. See Numerics for the derivation.

The byte storage interoperates with UNORM8 formats such as R8_UNORM. This is not a claim that every GPU's float-to-UNORM conversion uses identical rounding.

Trait Guide

Task API Semantics
Raw storage from_bits, to_bits, From<u8>, Into<u8> Lossless byte encoding; not a numeric cast.
Quantization from_f32, From<f32> Saturating, lossy, exact nearest-even rounding.
Decoding to_f32, Into<f32> Correctly rounded normalized value.
Initialization Default, ZERO, ONE, MIN, MAX Default is zero.
Ordering and maps Eq, Ord, Hash Byte equality, total numerical ordering; no NaNs to handle.
Arithmetic +, -, *, /, assignment forms Saturating UNORM arithmetic, owned or borrowed operands.
Const arithmetic saturating_add/sub/mul/div Same semantics as the corresponding operators.
Reductions Sum, Product Left folds; empty identities are zero and one.
Formatting Display, Debug Display is the normalized f32; Debug exposes the newtype and byte.
Serialization Serde Serialize, Deserialize Newtype f8 containing a u8, never a serialized float.

Product quantizes each step, so grouping or reordering can change results. Accumulate in a wider representation and quantize once when accuracy matters:

use f8::f8;

let weights = [f8::from_bits(1), f8::from_bits(128), f8::from_bits(128)];
assert_eq!(weights.iter().product::<f8>().to_bits(), 1);
let result = f8::from_f32(weights.iter().map(|x| x.to_f32()).product());
assert_eq!(result, f8::ZERO);

Buffers and Layout

Bulk conversion accepts caller-owned storage, does not allocate, and checks lengths before writing. Inputs need only their normal Rust alignment; vector widths, tails, and dispatch are internal details.

use f8::f8;

let mut packed = [f8::ZERO; 4];
f8::from_f32_slice(&[0.0, 0.25, 0.5, 1.0], &mut packed);
assert_eq!(f8::as_bytes(&packed), &[0, 64, 128, 255]);

let mut decoded = [0.0; 4];
f8::to_f32_slice(&packed, &mut decoded);

let mut bytes = [0, 128, 255];
f8::from_bytes_mut(&mut bytes)[0] = f8::ONE;
assert_eq!(bytes, [255, 128, 255]);

#[repr(transparent)] guarantees the layout/ABI of u8: one-byte size and alignment, with no invalid bit patterns or endianness dependence. as_bytes, as_bytes_mut, from_bytes, and from_bytes_mut provide safe, zero-copy slice views; Rust's ordinary borrowing rules still apply.

Platforms and Features

Rust 1.85+, edition 2024. No nightly compiler is required.

Feature Default Effect
serde Yes Allocation-free Serde support, with Serde's std feature disabled.
simd Yes AVX2 bulk encoding on eligible x86-64 hosts, portable Rust elsewhere.

Disable both for a dependency-free library. Enabling either feature never requires std or an allocator. Features of Serde selected by other dependencies are still unified by Cargo in the usual way.

Scalar encoding, decoding, and arithmetic use integers, with no soft-float runtime calls or lookup tables. The bulk encoder uses eight-lane integer AVX2 assembly for lengths of at least 32 on supported x86-64 hosts. The cached probe checks CPUID, OSXSAVE, and XCR0; unsupported CPUs never execute AVX2. An explicitly AVX2-enabled compilation skips detection. Short slices stay in Rust, as do AVX-512-enabled builds, where LLVM can exploit the wider instruction set.

Bare-metal x86-64, UEFI, and SGX do not perform runtime detection or compile the handwritten kernel. ARM, RISC-V, WebAssembly, other architectures, and Miri also use the portable encoder. Explicit compiler target features can still enable auto-vectorization and require the environment's corresponding state management. No ARM or WASM-specific assembly is claimed or required.

Bulk decoding uses a vectorizable exact-division loop where the target advertises SSE2, NEON, VFP2, RISC-V F, or WebAssembly floating-point operations, and integer bit construction elsewhere. Hardware division assumes Rust's default floating-point environment, including round-to-nearest-even. Scalar conversions and both encoding paths are independent of floating-point control registers.

See Performance for measurements, methodology, and commands. Do not ship binaries built with -C target-cpu=native to unknown CPUs.

Verification

cargo test --all-features
cargo test --no-default-features
cargo test --release --all-features -- --include-ignored
cargo clippy --all-targets --all-features -- -D warnings
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo +nightly miri test --all-features
cargo bench --bench convert

Tests cover all 65,536 operand pairs for each arithmetic operation and overload, every byte round trip, every quantization boundary and its neighbors, NaN payloads, all sign/exponent classes, randomized inputs, unaligned buffers, tails, panic-before-write behavior, const evaluation, and Serde tokens. The opt-in release test verifies 75,497,473 positive f32 bit patterns covering the entire nontrivial quantization range against an independent f64 reference, in both scalar and bulk paths. Miri checks the portable paths and slice casts; it does not interpret the assembly kernel.

About

8-bit floating-point number for Rust

Resources

Stars

10 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages