/machine/f16
F16
On Apple GPUs, 16-bit floats are faster than 32-bit for reasons that have little to do with peak throughput: shorter stalls and half the register pressure.
CUDA equivalent: none that matches cleanly. On NVIDIA you reach for fp16/bf16 to unlock tensor-core throughput, a peak-FLOPs argument. Here the argument is about latency and residency:
- Dependent-instruction stalls are shorter in 16-bit. A back-to-back dependent multiply pays ~1.84 cycles with F32 registers vs ~1.56 with F16; at low occupancy the dependent-FMA gap blows out to ~11.3 vs ~3.9 cycles. Measured, not spec'd; source: metal-benchmarks, ALU Bottlenecks↗.
- Half the register footprint, which on this machine means more resident simdgroups, better latency hiding, and more headroom before the spilling cliff.
- Half the memory traffic, which on a bandwidth-bound machine is often the whole ballgame.
So "use F16 everywhere you can tolerate it" is the local wisdom even where the ALU
throughput tables show F16 and F32 tied. The standard numerics discipline still
applies, and every serious kernel in the case studies
follows it: storage and traffic in 16-bit, accumulation in fp32. MLX's steel
templates default AccumType = float regardless of data type.
Related precision facts:
- bf16 is supported and behaves like F16 for the purposes above.
- fp8 was not real on M4-class hardware: reverse-engineering of Metal 4.1's tensor API (Rigel↗, tested on an M4 Max) found fp8 software-emulated at ~0.94× fp16 throughput. Half the bytes, none of the speed. On M5, the neural accelerators change this: block-scaled fp8/fp4 paths ship with macOS 27's Metal (WWDC26 330↗).
- I16/U16 get the same stall advantage over 32-bit integers; index arithmetic
in
ushortis a standing micro-optimization in production kernels (you'll seeushortloop counters throughout the GEMM case studies).