| """ |
| bitlinear.py — TriChronos-0.1B |
| Absmean ternary weight quantization (BitNet-style) with Straight-Through Estimator. |
| Per-token activation quantization applied before the linear projection. |
| |
| Reference: "The Era of 1-bit LLMs" (Ma et al., 2024) |
| """ |
|
|
| import math |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
|
|
|
|
| |
| |
| |
|
|
| def absmean_quantize(W: torch.Tensor, eps: float = 1e-6) -> torch.Tensor: |
| """ |
| Absmean ternary quantization. |
| |
| 1. Compute the scale γ = mean(|W|) (a single scalar per weight matrix) |
| 2. Divide W by γ, round to nearest integer, clip to {-1, 0, +1} |
| |
| During the forward pass only the ternary version is used; gradients flow |
| through unmodified (Straight-Through Estimator applied at call site). |
| """ |
| gamma = W.abs().mean().clamp(min=eps) |
| W_scaled = W / gamma |
| W_ternary = W_scaled.round().clamp(-1, 1) |
| return W_ternary, gamma |
|
|
|
|
| |
| |
| |
|
|
| def per_token_quant(x: torch.Tensor, bits: int = 8, eps: float = 1e-6) -> torch.Tensor: |
| """ |
| Symmetric per-token (per-row) quantization of activations to `bits` bits. |
| |
| x : (..., d_model) — last dim is the feature dimension, all other dims |
| are treated as independent "tokens". |
| |
| Each token is scaled independently so that its dynamic range fills the |
| integer grid [-2^(bits-1)+1, 2^(bits-1)-1]. We use absmax scaling |
| (same approach as LLM.int8()). |
| """ |
| qmax = 2 ** (bits - 1) - 1 |
| |
| scale = x.abs().amax(dim=-1, keepdim=True).clamp(min=eps) |
| x_scaled = (x / scale * qmax).round().clamp(-qmax, qmax) |
| |
| x_dequant = x_scaled / qmax * scale |
| return x_dequant |
|
|
|
|
| |
| |
| |
|
|
| class BitLinear(nn.Linear): |
| """ |
| Drop-in replacement for nn.Linear that: |
| • Keeps FP16/BF16 master weights (for optimizer stability) |
| • Quantizes weights to ternary {-1, 0, +1} during the forward pass |
| • Applies per-token activation quantization to the input |
| • Uses STE: gradients pass through the quantization step unchanged |
| |
| Usage is identical to nn.Linear: |
| layer = BitLinear(in_features, out_features, bias=False) |
| y = layer(x) |
| """ |
|
|
| def __init__( |
| self, |
| in_features: int, |
| out_features: int, |
| bias: bool = False, |
| act_bits: int = 8, |
| weight_eps: float = 1e-6, |
| ): |
| super().__init__(in_features, out_features, bias=bias) |
| self.act_bits = act_bits |
| self.weight_eps = weight_eps |
|
|
| |
| self.norm = nn.LayerNorm(in_features, elementwise_affine=True) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| |
| x = self.norm(x) |
|
|
| |
| |
| x_q = per_token_quant(x, bits=self.act_bits, eps=self.weight_eps) |
|
|
| |
| |
| |
| W_ternary, gamma = absmean_quantize(self.weight, eps=self.weight_eps) |
| |
| W_ste = self.weight + (W_ternary - self.weight).detach() |
| W_ste_scaled = W_ste * gamma |
|
|
| return F.linear(x_q, W_ste_scaled, self.bias) |
|
|
| |
| |
| |
|
|
| @torch.no_grad() |
| def ternary_weights(self) -> torch.Tensor: |
| """Return quantized weights as int8 tensor (values in {-1, 0, 1}).""" |
| W_ternary, _ = absmean_quantize(self.weight, eps=self.weight_eps) |
| return W_ternary.to(torch.int8) |
|
|
| @torch.no_grad() |
| def weight_scale(self) -> torch.Tensor: |
| """Return the absmean scale γ used during quantization.""" |
| _, gamma = absmean_quantize(self.weight, eps=self.weight_eps) |
| return gamma |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| torch.manual_seed(42) |
| layer = BitLinear(64, 128) |
| x = torch.randn(4, 16, 64) |
| y = layer(x) |
| print(f"Input shape : {x.shape}") |
| print(f"Output shape : {y.shape}") |
| assert y.shape == (4, 16, 128), "Unexpected output shape" |
|
|
| |
| loss = y.sum() |
| loss.backward() |
| print(f"Weight grad : {layer.weight.grad.shape} (STE working)") |
|
|
| |
| W_t = layer.ternary_weights() |
| unique_vals = W_t.unique().tolist() |
| print(f"Ternary unique values: {unique_vals} (expected subset of [-1, 0, 1])") |
| assert all(v in (-1, 0, 1) for v in unique_vals) |
|
|
| print("bitlinear.py - all checks passed OK") |
|
|