ArGrigorov's picture
Upload folder using huggingface_hub
e9c8366 verified
Raw
History Blame Contribute Delete
4.77 kB
"""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 # all-ones exponent
max_finite = 0.0
# Compute max finite value (exp = max_exp - 1, mant = all ones).
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 # signed zero
else:
# Subnormal: val = sign * mant / 2^mant_bits * 2^(1-bias)
val = sign * (mant / (2.0 ** mant_bits)) * (2.0 ** (1 - bias))
elif exp == max_exp:
# Inf/NaN — map to max finite (avoid NaN propagation).
val = sign * max_finite
else:
# Normalized: val = sign * (1 + mant/2^mb) * 2^(exp - bias)
val = sign * (1.0 + mant / (2.0 ** mant_bits)) * (2.0 ** (exp - bias))
lut[code] = val
return lut
# Precomputed LUTs (256 entries each, fp32).
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)
# Normalize by scale (broadcast depends on scale shape).
if scale.numel() == 1:
w_norm = w / scale
else:
reshape = [1] * w.dim()
reshape[0] = w.shape[0]
w_norm = w / scale.reshape(reshape)
# Nearest LUT entry.
# Clip to LUT range to avoid out-of-range.
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()] # [shape], fp32
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)