ArGrigorov's picture
Upload folder using huggingface_hub
e9c8366 verified
Raw
History Blame Contribute Delete
4.7 kB
"""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: sign(1) + exp(2) + mant(1). bias=1.
# 16 levels: code 0..15, bit 3=sign, bits 2-1=exp, bit 0=mant.
# Special: code 0 = +0, code 8 = -0.
# exp=0 (codes 0,1,8,9): subnormal: sign * (mant/2) * 2^(1-bias) = sign * mant/2 * 2^0 = sign*mant/2
# code 0: +0, code 1: +0.5, code 8: -0, code 9: -0.5
# exp=1 (codes 2,3,10,11): sign * (1 + mant/2) * 2^(1-1) = sign * (1+mant/2)
# code 2: +1.0, code 3: +1.5, code 10: -1.0, code 11: -1.5
# exp=2 (codes 4,5,12,13): sign * (1+mant/2) * 2^(2-1) = sign * (1+mant/2)*2
# code 4: +2.0, code 5: +3.0, code 12: -2.0, code 13: -3.0
# exp=3 (codes 6,7,14,15): sign * (1+mant/2) * 2^(3-1) = sign * (1+mant/2)*4
# code 6: +4.0, code 7: +6.0, code 14: -4.0, code 15: -6.0
FP4_E2M1_LUT: torch.Tensor = torch.tensor([
0.0, # 0: +0
0.5, # 1: +0.5 (subnormal)
1.0, # 2: +1.0
1.5, # 3: +1.5
2.0, # 4: +2.0
3.0, # 5: +3.0
4.0, # 6: +4.0
6.0, # 7: +6.0
-0.0, # 8: -0
-0.5, # 9: -0.5
-1.0, # 10: -1.0
-1.5, # 11: -1.5
-2.0, # 12: -2.0
-3.0, # 13: -3.0
-4.0, # 14: -4.0
-6.0, # 15: -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)
# Per-group absmax scale.
scale = grouped.abs().amax(dim=2).clamp(min=1e-8)
scale_exp = scale.unsqueeze(2).expand_as(grouped)
w_norm = grouped / scale_exp
# Nearest FP4 level.
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] # [out, in_padded]
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)
# E8M0: 8-bit exponent only (no sign, no mantissa). Used as MXFP block scale.
# Value = 2^(code - 127). code 0..255.
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()