| """fp4 — FP4 (E2M1) 4-bit floating-point format primitives. |
| |
| FP4 E2M1: 1 sign + 2 exponent + 1 mantissa. Bias=1. 16 levels (including |
| signed zero). Per-group FP8 (E4M3) scale (group_size=16) + F32 global scale. |
| |
| NVFP4 (NVIDIA): weights AND activations in FP4, per-16 FP8 scales, F32 global |
| weight_scale_2 + input_scale. Storage: 0.5 byte/weight + 1 byte FP8 scale per |
| 16 weights + 4 bytes global. |
| |
| MXFP4 (OCP MX): FP4 E2M1 + E8M0 (8-bit exponent only) shared block scale |
| (block_size=32). Single scale per block. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import torch |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| FP4_E2M1_LUT: torch.Tensor = torch.tensor([ |
| 0.0, |
| 0.5, |
| 1.0, |
| 1.5, |
| 2.0, |
| 3.0, |
| 4.0, |
| 6.0, |
| -0.0, |
| -0.5, |
| -1.0, |
| -1.5, |
| -2.0, |
| -3.0, |
| -4.0, |
| -6.0, |
| ], dtype=torch.float32) |
|
|
|
|
| def quantize_fp4(w: torch.Tensor, group_size: int = 16) -> tuple[torch.Tensor, torch.Tensor]: |
| """Quantize weights to FP4 E2M1 indices + per-group absmax scale. |
| |
| Args: |
| w: float [out, in] (or flat). in is padded to be divisible by group_size. |
| group_size: elements per scale group (NVFP4=16, MXFP4=32). |
| |
| Returns: |
| (idx, scale): idx int64 [out, in_padded] values [0,15], scale fp32 [out, num_groups]. |
| """ |
| w = w.detach().float() |
| out_features = w.shape[0] |
| in_features = w.shape[1] if w.dim() > 1 else w.numel() |
| if w.dim() > 1: |
| flat = w |
| else: |
| flat = w.reshape(1, -1) |
| out_features = 1 |
| pad = (group_size - (flat.shape[1] % group_size)) % group_size |
| if pad > 0: |
| flat = torch.nn.functional.pad(flat, (0, pad)) |
| in_padded = flat.shape[1] |
| num_groups = in_padded // group_size |
| grouped = flat.reshape(out_features, num_groups, group_size) |
| |
| scale = grouped.abs().amax(dim=2).clamp(min=1e-8) |
| scale_exp = scale.unsqueeze(2).expand_as(grouped) |
| w_norm = grouped / scale_exp |
| |
| lut = FP4_E2M1_LUT.to(w_norm.device) |
| diff = w_norm.unsqueeze(-1) - lut |
| idx = diff.abs().argmin(dim=-1).to(torch.int64) |
| idx = idx.reshape(out_features, in_padded) |
| return idx, scale |
|
|
|
|
| def dequantize_fp4( |
| idx: torch.Tensor, scale: torch.Tensor, group_size: int = 16, in_features: int | None = None |
| ) -> torch.Tensor: |
| """Reconstruct float weight from FP4 indices + per-group scale.""" |
| out_features = idx.shape[0] |
| in_padded = idx.shape[1] |
| num_groups = in_padded // group_size |
| lut = FP4_E2M1_LUT.to(idx.device) |
| w_norm = lut[idx] |
| w_grouped = w_norm.reshape(out_features, num_groups, group_size) |
| scale_exp = scale.to(torch.float32).unsqueeze(2).expand_as(w_grouped) |
| w_deq = (w_grouped * scale_exp).reshape(out_features, in_padded) |
| if in_features is not None and in_features < in_padded: |
| w_deq = w_deq[:, :in_features] |
| return w_deq |
|
|
|
|
| def pack_fp4(idx: torch.Tensor) -> torch.Tensor: |
| """Pack two 4-bit FP4 indices into one int8 byte.""" |
| assert idx.shape[-1] % 2 == 0, "in_features must be even for packing" |
| i = idx.to(torch.int16) |
| packed = (i[..., 0::2] << 4) | (i[..., 1::2] & 0x0F) |
| return packed.to(torch.int8) |
|
|
|
|
| def unpack_fp4(packed: torch.Tensor) -> torch.Tensor: |
| """Unpack int8 bytes into two 4-bit FP4 indices.""" |
| p = packed.to(torch.int16) |
| high = (p >> 4) & 0x0F |
| low = p & 0x0F |
| out = torch.stack([high, low], dim=-1).reshape(*p.shape[:-1], -1) |
| return out.to(torch.int64) |
|
|
|
|
| |
| |
| def _make_e8m0_lut() -> torch.Tensor: |
| lut = torch.zeros(256, dtype=torch.float64) |
| for code in range(256): |
| lut[code] = 2.0 ** (code - 127) |
| return lut.float() |
|
|
|
|
| E8M0_LUT: torch.Tensor = _make_e8m0_lut() |