| """fp8 — FP8 (E4M3 / E5M2) floating-point 8-bit format primitives. |
| |
| FP8 E4M3: 1 sign + 4 exponent + 3 mantissa. Bias=7. Wider mantissa → more |
| precision, narrower range. Used for weights/activations forward (NVIDIA H100+). |
| |
| FP8 E5M2: 1 sign + 5 exponent + 2 mantissa. Bias=15. Wider range, less precision. |
| Used for gradients / heavy-tailed distributions. |
| |
| Both encodable/decodable in pure PyTorch (no hardware FP8 tensor cores needed). |
| Dequant → fp32/fp16 matmul (like int8/NF4 strategies). |
| """ |
|
|
| from __future__ import annotations |
|
|
| import math |
|
|
| import torch |
|
|
|
|
| def _make_fp8_lut(exp_bits: int, mant_bits: int, bias: int) -> torch.Tensor: |
| """Build the 256-entry lookup table for an FP8 format. |
| |
| Returns fp32 tensor [256] of dequantized values for each 8-bit code. |
| Code layout: bit 7 = sign, bits 6..(7-exp) = exponent, low bits = mantissa. |
| Special: all-zero exponent with zero mantissa = +0; all-zero exp with nonzero |
| mantissa = subnormal; all-ones exponent = NaN/Inf (we map to ±448 for E4M3, |
| ±57344 for E5M2 — max finite, to avoid NaN in dequant). |
| """ |
| lut = torch.zeros(256, dtype=torch.float32) |
| exp_mask = (1 << exp_bits) - 1 |
| mant_mask = (1 << mant_bits) - 1 |
| max_exp = exp_mask |
| max_finite = 0.0 |
| |
| emax = (max_exp - 1) - bias |
| max_finite = (2.0 ** emax) * (1.0 + mant_mask / (2.0 ** mant_bits)) |
| for code in range(256): |
| sign = -1.0 if (code & 0x80) else 1.0 |
| exp = (code >> mant_bits) & exp_mask |
| mant = code & mant_mask |
| if exp == 0: |
| if mant == 0: |
| val = 0.0 |
| else: |
| |
| val = sign * (mant / (2.0 ** mant_bits)) * (2.0 ** (1 - bias)) |
| elif exp == max_exp: |
| |
| val = sign * max_finite |
| else: |
| |
| val = sign * (1.0 + mant / (2.0 ** mant_bits)) * (2.0 ** (exp - bias)) |
| lut[code] = val |
| return lut |
|
|
|
|
| |
| FP8_E4M3_LUT: torch.Tensor = _make_fp8_lut(exp_bits=4, mant_bits=3, bias=7) |
| FP8_E5M2_LUT: torch.Tensor = _make_fp8_lut(exp_bits=5, mant_bits=2, bias=15) |
|
|
|
|
| def quantize_fp8(w: torch.Tensor, lut: torch.Tensor, scale: torch.Tensor | None = None) -> tuple[torch.Tensor, torch.Tensor]: |
| """Quantize a float tensor to FP8 codes + per-group/per-channel scale. |
| |
| Args: |
| w: float tensor [out, in] (or any shape). |
| lut: 256-entry FP8 LUT (FP8_E4M3_LUT or FP8_E5M2_LUT). |
| scale: precomputed scale [out] (per-channel) or scalar. If None, computed |
| as absmax(w) / max(abs(lut)). |
| |
| Returns: |
| (codes, scale) where codes: uint8 [out, in], scale: fp32 [out] or scalar. |
| """ |
| w = w.detach().float() |
| if scale is None: |
| max_lut = lut.abs().amax().clamp(min=1e-12) |
| if w.dim() > 1: |
| max_abs = w.abs().amax(dim=tuple(range(1, w.dim()))) |
| scale = (max_abs / max_lut).clamp(min=1e-12) |
| else: |
| scale = (w.abs().amax() / max_lut).clamp(min=1e-12).reshape(1) |
| |
| if scale.numel() == 1: |
| w_norm = w / scale |
| else: |
| reshape = [1] * w.dim() |
| reshape[0] = w.shape[0] |
| w_norm = w / scale.reshape(reshape) |
| |
| |
| lut_max = lut.abs().amax().item() |
| w_norm = w_norm.clamp(-lut_max, lut_max) |
| diff = w_norm.unsqueeze(-1) - lut.to(w_norm.device) |
| codes = diff.abs().argmin(dim=-1).to(torch.uint8) |
| return codes, scale.to(torch.float32) |
|
|
|
|
| def dequantize_fp8(codes: torch.Tensor, scale: torch.Tensor, lut: torch.Tensor) -> torch.Tensor: |
| """Reconstruct float tensor from FP8 codes + scale. |
| |
| Args: |
| codes: uint8 [out, in] (or any shape). |
| scale: fp32 [out] (per-channel) or scalar. |
| lut: 256-entry FP8 LUT. |
| |
| Returns: |
| fp32 tensor same shape as codes. |
| """ |
| lut_dev = lut.to(codes.device) |
| w = lut_dev[codes.long()] |
| if scale.numel() == 1: |
| return w * scale.to(torch.float32) |
| reshape = [1] * w.dim() |
| reshape[0] = w.shape[0] |
| return w * scale.to(torch.float32).reshape(reshape) |
|
|
|
|
| def pack_fp8(codes: torch.Tensor) -> torch.Tensor: |
| """FP8 codes are already 1 byte each — no packing needed. Returns int8 view.""" |
| return codes.view(torch.int8) |
|
|
|
|
| def unpack_fp8(packed: torch.Tensor) -> torch.Tensor: |
| """Unpack int8 view back to uint8 codes.""" |
| return packed.view(torch.uint8) |