| """ |
| Packed 2-bit ternary, 3-bit symmetric int, and 4-bit weight formats. |
| |
| Reference implementation that reports real byte counts. |
| |
| Packing layout: |
| - 2-bit: 4 values packed into 1 byte. Each value is 2 bits representing |
| one of {-1, 0, +1, "n/a"} -> we use {0, 1, 2, 3} = {-1, 0, +1, unused}. |
| - 3-bit: 3 values packed into 1 byte (1 bit unused). Symmetric int in |
| range [-4, 3] (8 centroids). 2.67 bits/value effective, but the |
| unused bit is real overhead — see packed_size_bytes. |
| - 4-bit: 2 values packed into 1 byte. Each value is int4 in [-8, 7]. |
| - "fp16": no quant, weights stored as BF16 (the engine's working |
| dtype). 2 bytes per value. Used as a "no quant" baseline. |
| |
| scales: per-row fp16 scale, one per output row. Stored separately |
| from the packed weights. |
| |
| Byte accounting per row of an [out, in] Linear: |
| - 2-bit packed: out * ceil(in/4) bytes + out * 2 bytes (scales) |
| - 3-bit packed: out * ceil(in/3) bytes + out * 2 bytes (scales) |
| - 4-bit packed: out * ceil(in/2) bytes + out * 2 bytes (scales) |
| - FP16 (BF16): out * in * 2 bytes (no scales needed) |
| """ |
| from __future__ import annotations |
| from typing import Tuple |
| import torch |
|
|
|
|
| def quantize_packed(w: torch.Tensor, bits: int) -> Tuple[torch.Tensor, torch.Tensor]: |
| """Quantize a 2D weight matrix and return (packed_bytes, per_row_scales). |
| |
| `packed_bytes` is a uint8 tensor of shape [out, ceil(in/pack)]. |
| `scales` is fp16 [out]. |
| """ |
| assert w.is_floating_point() and w.dim() == 2 |
| out, in_dim = w.shape |
| w32 = w.to(torch.float32) |
| if bits == 2: |
| |
| max_abs = w32.abs().amax(dim=1, keepdim=True).clamp(min=1e-8) |
| scale = max_abs.squeeze(1) |
| w_scaled = w32 / max_abs |
| q = torch.zeros_like(w_scaled, dtype=torch.int8) |
| q[w_scaled > 0.33] = 1 |
| q[w_scaled < -0.33] = -1 |
| q01 = (q + 1).to(torch.int64) |
| pad = (4 - in_dim % 4) % 4 |
| if pad: |
| q01 = torch.cat([q01, torch.zeros(out, pad, dtype=torch.int64)], dim=1) |
| in_packed = q01.shape[1] // 4 |
| packed = torch.zeros(out, in_packed, dtype=torch.uint8) |
| for j in range(4): |
| packed |= (q01[:, j::4].to(torch.uint8) << (2 * j)) |
| return packed, scale.to(torch.float16) |
| if bits == 3: |
| |
| |
| |
| |
| |
| |
| max_abs = w32.abs().amax(dim=1, keepdim=True).clamp(min=1e-8) |
| scale = (max_abs / 4.0).squeeze(1) |
| w_scaled = (w32 / scale.unsqueeze(1)).round().clamp(-4, 3) |
| q01 = (w_scaled + 4).to(torch.int64) |
| |
| pad = (2 - in_dim % 2) % 2 |
| if pad: |
| q01 = torch.cat([q01, torch.zeros(out, pad, dtype=torch.int64)], dim=1) |
| |
| |
| in_padded = q01.shape[1] |
| pairs = q01.reshape(out, in_padded // 2, 2) |
| v0 = pairs[:, :, 0].to(torch.uint8) & 0x7 |
| v1 = pairs[:, :, 1].to(torch.uint8) & 0x7 |
| packed = v0 | (v1 << 3) |
| return packed, scale.to(torch.float16) |
| if bits == 4: |
| max_abs = w32.abs().amax(dim=1, keepdim=True).clamp(min=1e-8) |
| scale = (max_abs / 7.0).squeeze(1) |
| w_scaled = (w32 / scale.unsqueeze(1)).round().clamp(-8, 7) |
| q01 = (w_scaled + 8).to(torch.int64) |
| pad = (2 - in_dim % 2) % 2 |
| if pad: |
| q01 = torch.cat([q01, torch.zeros(out, pad, dtype=torch.int64)], dim=1) |
| in_packed = q01.shape[1] // 2 |
| packed = torch.zeros(out, in_packed, dtype=torch.uint8) |
| packed |= (q01[:, 0::2].to(torch.uint8) & 0xF) |
| packed |= (q01[:, 1::2].to(torch.uint8) & 0xF) << 4 |
| return packed, scale.to(torch.float16) |
| if bits == 16: |
| |
| |
| |
| |
| scale = torch.ones(out, dtype=torch.float16) |
| |
| w_bf16 = w.to(torch.bfloat16).contiguous() |
| packed = w_bf16.view(torch.uint8).reshape(out, in_dim * 2).contiguous() |
| return packed, scale |
| raise NotImplementedError(f"bits={bits}") |
|
|
|
|
| def dequantize_packed(packed: torch.Tensor, scales: torch.Tensor, |
| out_dim: int, in_dim: int, bits: int) -> torch.Tensor: |
| """Inverse of quantize_packed. Returns BF16 weight.""" |
| if bits == 2: |
| out, in_packed = packed.shape |
| q01 = torch.zeros(out, in_packed * 4, dtype=torch.int64) |
| for j in range(4): |
| q01[:, j::4] = ((packed >> (2 * j)) & 0x3).to(torch.int64) |
| q01 = q01[:, :in_dim] |
| q = (q01 - 1).to(torch.float32) |
| return (q * scales.to(torch.float32).unsqueeze(1)).to(torch.bfloat16) |
| if bits == 3: |
| out, in_packed = packed.shape |
| |
| v0 = (packed & 0x7).to(torch.int64) |
| v1 = ((packed >> 3) & 0x7).to(torch.int64) |
| |
| q01 = torch.stack([v0, v1], dim=-1).reshape(out, in_packed * 2) |
| q01 = q01[:, :in_dim] |
| q = (q01 - 4).to(torch.float32) |
| return (q * scales.to(torch.float32).unsqueeze(1)).to(torch.bfloat16) |
| if bits == 4: |
| out, in_packed = packed.shape |
| q01 = torch.zeros(out, in_packed * 2, dtype=torch.int64) |
| q01[:, 0::2] = (packed & 0xF).to(torch.int64) |
| q01[:, 1::2] = ((packed >> 4) & 0xF).to(torch.int64) |
| q01 = q01[:, :in_dim] |
| q = (q01 - 8).to(torch.float32) |
| return (q * scales.to(torch.float32).unsqueeze(1)).to(torch.bfloat16) |
| if bits == 16: |
| |
| flat = packed.reshape(out_dim, in_dim * 2) |
| t = flat.view(torch.uint8).reshape(out_dim, in_dim, 2).view(torch.bfloat16).reshape(out_dim, in_dim).clone() |
| return t |
| raise NotImplementedError(f"bits={bits}") |
|
|
|
|
| def packed_size_bytes(out_dim: int, in_dim: int, bits: int) -> int: |
| """Real on-disk / on-RAM byte count for a packed weight tensor (no scales).""" |
| if bits == 2: |
| return out_dim * ((in_dim + 3) // 4) |
| if bits == 3: |
| |
| return out_dim * ((in_dim + 1) // 2) |
| if bits == 4: |
| return out_dim * ((in_dim + 1) // 2) |
| if bits == 16: |
| return out_dim * in_dim * 2 |
| raise NotImplementedError |
|
|
|
|
| def total_packed_size_bytes(out_dim: int, in_dim: int, bits: int) -> int: |
| """packed weights + per-row fp16 scales (no scales needed at 16).""" |
| if bits == 16: |
| return packed_size_bytes(out_dim, in_dim, bits) |
| return packed_size_bytes(out_dim, in_dim, bits) + out_dim * 2 |
|
|
|
|