Conversion
Every conversion reduces to one function, Microfloats.cvt. Constructors, convert and broadcasts only resolve defaults and call it, so there is one place to specialize a conversion and one place for a device backend to override it.
| Direction | Scalar | Vector |
|---|---|---|
into a Microfloat | cvt(T, x, mode, policy) | cvt(SVector{N,T}, xs, mode, policy), cvt(NVector{T,N}, xs, mode, policy) |
out of a Microfloat | cvt(F, x) | cvt(SVector{N,F}, xs) |
Narrowing takes a rounding mode and an overflow policy as positional arguments, so methods can dispatch on them. Widening into Float16, BFloat16, Float32 or Float64 takes neither: every Microfloat is exactly representable in BFloat16.
julia> using Microfloats: cvt, SAT
julia> cvt(Float8_E4M3FN, 1000f0, RoundNearest, SAT)
Float8_E4M3FN(448.0)
julia> cvt(Float32, Float8_E4M3FN(1.5))
1.5f0Microfloats.cvt — Function
cvt(::Type{T}, x, mode::RoundingMode, policy::OverflowPolicy) -> TCentral conversion funnel. Every scalar conversion into a Microfloat — constructors, convert, broadcasts, and the packed vector paths — reduces to a call of this function, with the rounding mode and overflow policy as positional, dispatchable arguments.
cvt is the extension surface for optimized conversions. To specialize, add a method on any subset of (T, typeof(x), mode, policy):
- Bit-twiddling / table specializations add ordinary methods, e.g.
Microfloats.cvt(::Type{Float8_E4M3}, x::Float4_E2M1FN, ::RoundingMode, ::OverflowPolicy). See@cvt_tablefor a generated lookup-table shortcut. - Device backends (package extensions) use overlay method tables (e.g.
CUDACore.@device_override) on exactly the(T, source, mode, policy)signatures the hardware supports natively; every other combination falls through to the portable methods below.
The always-correct reference path is cvt_generic; specialized methods that need a partial fallback should call it (not cvt, which on overlay method tables would recurse into the override itself).
cvt(::Type{F}, x::Microfloat) -> FWidening half of the conversion funnel: every conversion out of a Microfloat into Float16, BFloat16, Float32 or Float64 reduces to a call of this method, the same way conversions into a Microfloat reduce to the four-argument form.
Every Microfloat is exactly representable in BFloat16 (at most 7 significand bits and 8 exponent bits), and so in Float32 and Float64: widening never rounds, which is why this form takes no rounding mode or overflow policy. Float16 has a narrower exponent range than some formats (Float8_E8M0FNU); it receives the exact Float32 value rounded by Float16(::Float32).
Like the narrowing form this is the extension surface for device backends, which override it for the destinations their hardware widens to natively.
cvt(::Type{SVector{N,T}}, xs::NTuple{N,Any}, mode, policy) -> SVector{N,T}
cvt(::Type{NVector{T,N}}, xs::NTuple{N,Any}, mode, policy) -> NVector{T,N}Vector forms of the conversion funnel: convert N source lanes in one call, into one byte per lane (SVector) or densely packed (BitPacking.NVector). The SVector default is lanewise (cvt_lanes); the NVector default converts through the SVector form and packs. Packing is layout only and folds away, so specialize the SVector form for multi-lane hardware conversions (e.g. PTX cvt x2 instructions in device overlays): that serves both destinations. Specialize the NVector form only for conversions that work on dense storage directly, such as bit-twiddling over packed sources.
Source containers (SVector, NVector, any StaticArray vector) normalize to NTuple first; packed→packed specializations may intercept the NVector-source signature before it is unpacked.
cvt(::Type{SVector{N,F}}, xs::NTuple{N,T}) -> SVector{N,F}Vector form of the widening funnel: N lanes of microfloat T to N wide lanes (F is Float16, BFloat16, Float32 or Float64). Packed (NVector) and unpacked (SVector) sources normalize to a tuple of lanes; the default is lanewise through the scalar widening funnel. Specialize it for multi-lane hardware conversions, e.g. PTX cvt.rn.bf16x2.e4m3x2 in device overlays. Packing and unpacking a source is layout only and folds away, so a specialization on the tuple form serves every source container.
Microfloats.cvt_generic — Function
cvt_generic(::Type{T}, x::Float32, mode::RoundingMode, policy::OverflowPolicy) -> TThe generic reference implementation behind cvt: bit-level rounding from Float32 into any Microfloat layout, for every supported rounding mode and overflow policy. Specialized cvt methods (and device overrides) call this directly when their fast path does not apply.
Microfloats.cvt_lanes — Function
cvt_lanes(::Type{SVector{N,T}}, xs::NTuple{N,Any}, mode, policy) -> SVector{N,T}
cvt_lanes(::Type{NVector{T,N}}, xs::NTuple{N,Any}, mode, policy) -> NVector{T,N}Reference lanewise implementation of the vector conversion funnel: converts each lane through the scalar cvt funnel (so per-lane specializations and device overrides still apply), then builds the vector. Vectorized cvt specializations call this when their fast path does not cover the requested combination.
Microfloats.WideFloat — Type
WideFloatThe floating-point destinations of the widening funnel: Float16, BFloat16, Float32 and Float64.
Vectors of lanes
Two static vector types hold N values of one format:
SVector{N,T}(StaticArrays) gives each value its own byte.NVector{T,N}(BitPacking) packs the values densely,bitwidth(T)bits each.
For 8-bit formats the two have the same bits. They differ for narrower formats, and both layouts occur in hardware: a 4-bit pair is one byte, while a 6-bit pair is two bytes with each value in the low six bits of its own byte. The aliases name the layout NVIDIA's packed types (__nv_fp8x2_e4m3, __nv_fp6x2_e2m3, __nv_fp4x2_e2m1, …) and the PTX cvt instructions use:
| Alias | Type | Storage |
|---|---|---|
Float8x2_E4M3FN, Float8x4_E4M3FN | NVector{Float8_E4M3FN,N} | UInt16, UInt32 |
Float8x2_E5M2, Float8x4_E5M2 | NVector{Float8_E5M2,N} | UInt16, UInt32 |
Float8x2_E8M0FNU, Float8x4_E8M0FNU | NVector{Float8_E8M0FNU,N} | UInt16, UInt32 |
Float6x2_E2M3FN, Float6x4_E2M3FN | SVector{N,Float6_E2M3FN} | one byte per value |
Float6x2_E3M2FN, Float6x4_E3M2FN | SVector{N,Float6_E3M2FN} | one byte per value |
Float4x2_E2M1FN, Float4x4_E2M1FN | NVector{Float4_E2M1FN,N} | UInt8, UInt16 |
Float16x2, BFloat16x2, … | SVector{N,Float16}, SVector{N,BFloat16} | two bytes per value |
Densely packed 6-bit storage, as in memory, is NVector{Float6_E2M3FN,N}.
Both vector types construct from any static vector of lanes, and widen back:
julia> using Microfloats: SVector, NVector, SAT
julia> xs = SVector(0.5f0, 1f0, -6f0, 100f0);
julia> packed = NVector{Float4_E2M1FN,4}(xs; overflow=SAT);
julia> sizeof(packed)
2
julia> Tuple(SVector{4,Float32}(packed))
(0.5f0, 1.0f0, -6.0f0, 6.0f0)Packing is layout only. The packed narrowing form converts through the SVector form and packs, and packed widening sources unpack to a tuple of lanes, so a specialization of the unpacked form serves both.
Specializing a conversion
Optimized implementations are ordinary methods on the funnel, chosen by dispatch:
Microfloats.cvt_genericis the bit-level reference path.Microfloats.@cvt_tableregisters a lookup table for one pair of microfloat types. Every built-in pair has one.- Hand-written methods, such as the exact
Float4_E2M1FNtoFloat8_E4M3FNwidening, which also converts packed storage to packed storage directly. - Device overrides in package extensions.
Microfloats.@cvt_table — Macro
@cvt_table Src => DstRegister an optimized lookup-table method on the conversion funnel cvt for converting microfloat Src values to microfloat Dst.
Expands to a @generated method of Microfloats.cvt whose lookup table is computed lazily — once per (mode, policy) combination actually used — by running every Src bit pattern through cvt_generic, so results are identical to the generic path but cost a single 2^bitwidth(Src)-entry table lookup. Combinations where the generic path throws (e.g. signed source into unsigned target) keep the runtime path and its errors.
Invoke after both types are defined. Microfloats registers tables for all pairs of built-in types; user-defined @microfloat types can opt in:
@microfloat MyFloat6 exponent=3 significand=2
Microfloats.@cvt_table MyFloat6 => Float8_E4M3
Microfloats.@cvt_table Float8_E4M3 => MyFloat6To hand-optimize a pair instead (e.g. branch-free bit-twiddling), define the Microfloats.cvt method for it directly rather than invoking @cvt_table for that pair.
CUDA
With CUDACore loaded, kernels and broadcasts lower conversions to the native PTX cvt instructions where the compile target has them, and run the generic path otherwise. The choice is made when the kernel is compiled.
| Conversion | Operands | Target |
|---|---|---|
to Float8_E4M3FN, Float8_E5M2 | Float32, Float16 | sm_89 and newer |
to Float8_E4M3FN, Float8_E5M2 | BFloat16 | sm_100 and newer, arch or family target |
to Float6_E2M3FN, Float6_E3M2FN, Float4_E2M1FN | Float32, Float16, BFloat16 | sm_100 and newer, arch or family target |
to Float8_E8M0FNU (RoundToZero, RoundUp) | Float32, BFloat16 | sm_100 and newer, arch or family target |
from Float8_E4M3FN, Float8_E5M2 | Float16 | sm_89 and newer |
| from any of the six formats | Float16, BFloat16, Float32 | sm_100 and newer, arch or family target |
Float8_E8M0FNU has no native Float16 form. Float32 widens through BFloat16.
Hardware narrowing always saturates, so native narrowing applies to the SAT policy with RoundNearest (or the two listed modes for Float8_E8M0FNU); everything else takes the generic path. Vector forms use one instruction per two lanes for any even N. Native and generic results agree bit for bit, apart from NaN payloads.