Spaces:
Sleeping
Sleeping
| """ | |
| NormalFloat4 (NF4) Block-Wise Weight Quantization. | |
| NF4 is the quantization format from the QLoRA paper. Instead of spacing | |
| quantization levels evenly (like INT8 absmax), NF4 places its 16 levels | |
| at quantiles of the standard normal distribution. Since neural network | |
| weights are approximately normal, this puts more levels near zero (where | |
| most weights cluster) and fewer in the tails — capturing more information | |
| per bit than uniform spacing. | |
| Block-wise quantization computes one absmax scale per block of 64 weights, | |
| finer than per-channel, which reduces quantization error. Each weight is | |
| normalized by its block's absmax, then snapped to the nearest NF4 level. | |
| Two 4-bit indices are packed into one uint8 byte, so the packed tensor is | |
| half the size of the original element count. Combined with 4-bit precision, | |
| NF4 uses ~4.5 bits per weight (4 bits + amortized scale overhead), roughly | |
| a 4x compression vs float16. | |
| """ | |
| import math | |
| import torch | |
| import torch.nn as nn | |
| # 16 quantization levels from the QLoRA paper, computed as quantiles of N(0,1) | |
| # mapped to [-1, 1]. Asymmetric: 8 non-positive (including 0) + 8 positive. | |
| NF4_LEVELS = [ | |
| -1.0, | |
| -0.6961928009986877, | |
| -0.5250730514526367, | |
| -0.39491748809814453, | |
| -0.28444138169288635, | |
| -0.18477343022823334, | |
| -0.09105003625154495, | |
| 0.0, | |
| 0.07958029955625534, | |
| 0.16093020141124725, | |
| 0.24611230194568634, | |
| 0.33791524171829224, | |
| 0.44070982933044434, | |
| 0.5626170039176941, | |
| 0.7229568362236023, | |
| 1.0, | |
| ] | |
| def _snap_to_nf4(normalized): | |
| """Snap each value in normalized tensor (in [-1, 1]) to the nearest NF4 level index.""" | |
| levels = torch.tensor(NF4_LEVELS, dtype=normalized.dtype, device=normalized.device) | |
| # Shape: (*normalized.shape, 16) — distance to each level | |
| distances = (normalized.unsqueeze(-1) - levels).abs() | |
| return distances.argmin(dim=-1).to(torch.uint8) | |
| def _pack_uint4(indices): | |
| """Pack pairs of 4-bit indices into uint8. Input length must be even.""" | |
| # Cast to int32 before bitwise ops — MPS does not support uint8 bit-shifts. | |
| indices = indices.reshape(-1).to(torch.int32) | |
| high = (indices[0::2] << 4) & 0xFF | |
| low = indices[1::2] & 0xFF | |
| return (high | low).to(torch.uint8) | |
| def _unpack_uint4(packed, num_elements): | |
| """Unpack uint8 into 4-bit indices.""" | |
| # Cast to int32 before bitwise ops — MPS does not support uint8 bit-shifts. | |
| packed_i32 = packed.to(torch.int32) | |
| high = (packed_i32 >> 4) & 0x0F | |
| low = packed_i32 & 0x0F | |
| # Interleave high and low | |
| unpacked = torch.stack([high, low], dim=-1).reshape(-1).to(torch.uint8) | |
| return unpacked[:num_elements] | |
| def nf4_quantize(tensor, block_size=64): | |
| """ | |
| Quantize a float tensor to NF4 using block-wise absmax scaling. | |
| Args: | |
| tensor: Float tensor (typically a weight matrix) | |
| block_size: Number of elements per quantization block | |
| Returns: | |
| (packed_uint8, scales) | |
| - packed_uint8: Two 4-bit NF4 indices packed per byte | |
| - scales: One absmax scale per block | |
| """ | |
| if block_size % 2 != 0: | |
| raise ValueError("block_size must be even for uint4 packing") | |
| original_shape = tensor.shape | |
| flat = tensor.reshape(-1).float() | |
| n = flat.numel() | |
| # Pad to multiple of block_size | |
| pad = (block_size - n % block_size) % block_size | |
| if pad > 0: | |
| flat = torch.cat([flat, torch.zeros(pad, device=flat.device)]) | |
| # Reshape into blocks | |
| blocks = flat.reshape(-1, block_size) | |
| # One absmax scale per block | |
| scales = blocks.abs().amax(dim=1) | |
| scales = torch.clamp(scales, min=1e-8) | |
| # Normalize each block to [-1, 1] | |
| normalized = blocks / scales.unsqueeze(1) | |
| # Snap to nearest NF4 level | |
| indices = _snap_to_nf4(normalized) | |
| # Pack pairs of 4-bit indices into uint8 | |
| packed = _pack_uint4(indices) | |
| return packed, scales | |
| def nf4_dequantize(packed, scales, original_shape, block_size=64): | |
| """ | |
| Reconstruct float tensor from NF4 packed data + scales. | |
| Args: | |
| packed: uint8 tensor with two 4-bit NF4 indices per byte | |
| scales: One absmax scale per block | |
| original_shape: Original tensor shape before quantization | |
| block_size: Block size used during quantization | |
| Returns: | |
| Reconstructed float tensor with original_shape | |
| """ | |
| n = math.prod(original_shape) | |
| # Pad count to match what was used during quantization | |
| pad = (block_size - n % block_size) % block_size | |
| total = n + pad | |
| # Unpack indices | |
| indices = _unpack_uint4(packed, total) | |
| # Look up NF4 levels | |
| levels = torch.tensor(NF4_LEVELS, dtype=torch.float32, device=packed.device) | |
| values = levels[indices.long()] | |
| # Reshape into blocks, multiply by scales | |
| blocks = values.reshape(-1, block_size) | |
| dequantized = blocks * scales.unsqueeze(1) | |
| # Flatten and trim padding | |
| return dequantized.reshape(-1)[:n].reshape(original_shape) | |
| class NF4Linear(nn.Module): | |
| """Drop-in replacement for nn.Linear that stores NF4-packed weights and dequantizes on forward.""" | |
| def __init__(self, in_features, out_features, packed_weight, scales, weight_shape, block_size, bias=None): | |
| super().__init__() | |
| self.in_features = in_features | |
| self.out_features = out_features | |
| self.register_buffer("_weight_shape", torch.tensor(list(weight_shape), dtype=torch.long)) | |
| self.register_buffer("_block_size", torch.tensor(block_size, dtype=torch.long)) | |
| self.register_buffer("packed_weight", packed_weight) | |
| self.register_buffer("scales", scales) | |
| if bias is not None: | |
| self.register_buffer("bias", bias) | |
| else: | |
| self.bias = None | |
| def from_linear(cls, linear, block_size=64): | |
| """Create an NF4Linear from an existing nn.Linear.""" | |
| weight = linear.weight.data.float() | |
| packed, scales = nf4_quantize(weight, block_size=block_size) | |
| bias = linear.bias.data if linear.bias is not None else None | |
| return cls(linear.in_features, linear.out_features, packed, scales, weight.shape, block_size, bias) | |
| def forward(self, x): | |
| w = nf4_dequantize(self.packed_weight, self.scales, tuple(self._weight_shape.tolist()), self._block_size.item()).to(x.dtype) | |
| return nn.functional.linear(x, w, self.bias) | |
| def quantize_model_nf4(model, block_size=64): | |
| """Walk the model and replace all nn.Linear layers with NF4Linear. | |
| Skips Linear layers whose weights are tied to an Embedding (e.g. a | |
| shared output head), since quantizing them would break the tie and | |
| produce a mismatch between embedding lookups and output projections. | |
| """ | |
| # Collect data_ptrs of all Embedding weights so we can detect ties | |
| tied_ptrs = { | |
| m.weight.data_ptr() | |
| for m in model.modules() | |
| if isinstance(m, nn.Embedding) | |
| } | |
| _quantize_nf4_recursive(model, block_size, tied_ptrs) | |
| def _quantize_nf4_recursive(module, block_size, tied_ptrs=frozenset()): | |
| for name, child in module.named_children(): | |
| if isinstance(child, nn.Linear): | |
| if child.weight.data_ptr() in tied_ptrs: | |
| continue # skip weight-tied layers | |
| setattr(module, name, NF4Linear.from_linear(child, block_size=block_size)) | |
| else: | |
| _quantize_nf4_recursive(child, block_size, tied_ptrs) | |