f16 ↔ f32 conversion — tracking (status + roadmap)
- Dominant language
- Rust
- Stars
- 12
- Forks
- 2
- Avg merge
- 13h 55m
- Merged PRs (30d)
- 28
Description
Tracking issue for f16 (IEEE-754 binary16) ↔ f32 support in magetypes: what's shipped, the design, measured performance, the Rust-1.96 intrinsic landscape, and open opportunities.
## Status: shipped in **v0.9.26** (crates.io, 2026-06-01)
### API (methods only — no free functions)
**Register / single-vector** (value methods on the vector types; tokenless-generic, **never summon** — the value's backend determines the instructions; pure branchless safe arithmetic, no intrinsics, arch-independent):
- `i32x4::::f16_to_f32(self) -> f32x4` — decode 4 f16 bit patterns (low 16 bits of each lane).
- `f32x4::::to_f16(self) -> i32x4` — encode 4 f32 (RTNE) into f16 bit patterns.
**Slice** (token methods on the sealed `F16Convert` trait; a whole-slice op **may summon-up** the best tier once per call — amortized over every lane):
- `token.f16_to_f32_slice(&[u16], &mut [f32])`
- `token.f32_to_f16_slice(&[f32], &mut [u16])`
### Backend dispatch (the token determines the floor; slices summon-up)
| Token | Path |
|---|---|
| `X64V3Token` | 8-wide F16C (`_mm256_cvtph_ps` / `_mm256_cvtps_ph`); with the `avx512` feature on a V4 CPU, **summons-up to 16-wide AVX-512F** (`_mm512_cvtph_ps` / `_mm512_cvtps_ph`) for the slice bulk, 8/4-wide F16C tail. A V4-tier holder reaches the same via `token.v3()`. |
| `NeonToken` | native NEON-f16 (`vcvt_f32_f16` / `vcvt_f16_f32`) when the CPU proves `fp16` (summons `Arm64V2Token`); gated `#[rustversion::since(1.94)]`; else software. |
| anything else | branchless software kernel. |
Design notes:
- **Summon is fine for a whole-slice op** (the cached `summon()` ~1.3 ns is amortized over the slice); **never inside a single-vector op** (register methods stay tokenless-generic).
- The "plain V4 path" (V4/V4x/FP16 tokens implementing `F16Convert` directly) was tried and **removed** — redundant given summon-up, and it forced an expensive `F32x4Convert`+`I32x4Backend` delegation for three tokens (~40% slower clean `magetypes --features avx512` build) for zero runtime gain.
- AVX-512 **FP16** (`avx512fp16`) is intentionally **not** used for the *conversion* — its `vcvtph2psx`/`vcvtps2phx` match AVX-512F throughput for f16↔f32 while being far less available. Its real value is native f16 *arithmetic* (see Open below).
### Correctness (bit-exact, verified exhaustively)
`magetypes/tests/convert_f16_exhaustive.rs`:
- decode: **bit-identical** to a scalar IEEE reference for all 65 536 f16 (incl. subnormals/Inf; reproduces NaN payloads).
- encode: **bit-identical** RTNE for all finite/Inf f32 (subnormal-flush + ±Inf saturation); NaN → some quiet f16 NaN (payload may differ — documented).
- All HW paths (F16C, AVX-512F, NEON-f16) verified **bit-identical to the software path** with the same benign **NaN-only** divergence; the 16-wide zmm `vcvtph2ps`/`vcvtps2ph` emission is confirmed in `objdump`.
### Performance (Zen 4 / 7950X — `benchmarks/f16_convert_zen4-7950x_2026-06-01.md`)
- 16-wide vs 8-wide F16C: a **modest** win, **not 2×** — decode ≈1.2–1.5×, encode ≈1.5–1.8× compute-bound (L1-resident); **≈parity** once memory-bandwidth-bound. (Zen 4 double-pumps AVX-512 on 256-bit units, so the 512-bit convert is internally 2×256-bit; only the halved loop/instruction count buys the win.)
- The once-per-slice `summon()` is **free**: production `f16c` (V3 summons-up) ≈ `v4` (direct) within 1%.
- HW vs software: **10–21×**.
- **Unmeasured:** parts with a *native* 512-bit datapath (Intel Skylake-X / Ice Lake / Sapphire Rapids server) should gain more — no such hardware to measure on.
### Relevant commits
- v0.9.25: original f16 arc (branchless software + F16C + NEON-f16, toolchain-version gate, exhaustive tests).
- v0.9.26: `9c63dc2` (AVX-512F 16-wide via summon-up) → `0d6ddaf0`/`adff4bd0` (plain-V4-path added then removed) → slice summon-up final.
---
## Rust 1.96 intrinsic landscape (DB regenerated in `8d592fee`)
`docs/intrinsics/complete_intrinsics.csv` was rebuilt against 1.96 stdarch (also fixed two extractor bugs: `pub const fn` not matched, and `#[rustc_const_unstable]` mis-read as unstable). f16-relevant **stable** intrinsics now in the DB:
| Arch | Feature | Stable | Notes |
|---|---|--:|---|
| x86 | **`avx512fp16`** | **893** | **native f16 arithmetic** — `add/mul/fmadd/sqrt/max/min_ph`, compares, reductions, `cvtxph`/`cvtxps`. **Stable since 1.94.** |
| x86 | `avx512f` | 21 | 16-wide convert (used by the shipped V4 path) |
| x86 | `f16c` | 4 | classic convert (the 8-wide path) |
| x86 | `avxneconvert` | 4 | `_mm*_cvtnee/cvtneoph_ps` — f16→f32 **from memory at AVX2 level** (no AVX-512) |
| ARM | `neon` (fp16) | 252 | half-precision arithmetic + convert |
New f16 in 1.96 (ARM): `vamax_f16`/`vamin_f16`, `vscale_f16`, `vluti2_*_f16`.
**Caveat:** the f16 *primitive type* and the f16-typed memory ops (`_mm512_loadu_ph`/`_mm_load_sh`, `*f16`) are **still `unstable`** in 1.96 (`#![feature(f16)]`). f16 vector *arithmetic* on `__m512h` is stable; scalar f16 load/store is not.
**Pointer-using new/newly-stable intrinsics needing safe wrappers:** 18 (ARM `vldap1*` RCPC3 load-acquire, x86 `_movrs_*`, ARM `__rndr`, x86 `_mm*_cvtnee/cvtneoph_ps`). **None** are in an archmage token tier or needed by the f16 work, and none are covered by `safe_unaligned_simd` 0.2.5. No action required now.
---
## Open / future
- [ ] **Native `f16xN` arithmetic type** (the main opportunity unlocked by 1.96). Back it with the 893 stable `avx512fp16` ops (`Avx512Fp16Token` tier) + the 252 stable NEON-fp16 ops (`Arm64V2Token`/`fp16`), software fallback elsewhere. Lets pipelines do the *math* in f16 instead of round-tripping to f32.
- Memory I/O caveat: f16 scalar `loadu_ph` is still nightly, so loads/stores go through `__m512i`/bitcast via the already-safe-wrapped integer si-loads until the `f16` primitive stabilizes.
- [ ] When `f16` + `loadu_ph`/`load_sh` stabilize: add `safe_unaligned_simd` wrappers for the f16 pointer memory ops, then the f16 type can load/store natively.
- [ ] **AVX-NE-CONVERT** (`_mm*_cvtneeph_ps`) — an AVX2-level f16→f32 path (no AVX-512). Only worth it if we add an `avxneconvert` tier; would need a safe pointer wrapper (it loads from `*__m256h`).
- [ ] Measure the 16-wide path on an Intel **native-512-bit-datapath** part (Skylake-X / Ice Lake / Sapphire Rapids) to confirm the expected larger-than-Zen4 win.
## Cross-refs
- #45 (F32x8Convert + Convert-trait gaps for X64V4Token) — related token/convert-coverage audit.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.