RegalInvader's picture
Release: Marco-Nano-Instruct 1-, 2- and 3-bit GGUF for phones
e5235fb verified
Raw History Blame Contribute Delete
5 kB
"""GPTQ-style error-compensated ternary latents (T5.0).
GPTQ quantises a weight's input columns one at a time and pushes each column's rounding error onto the columns not
yet quantised, weighted by the inverse Hessian of the layer's inputs (H = XᵀX). Here the quantiser is the student's
own (Q-A: ternary codes, per-row absmean scale per group of 128 input columns), and the result is a *latent*: the
trainer re-derives scale and codes from it on every forward (`fake_quant`). So the latent of each group is the
error-compensated values GPTQ actually rounded. The group's scale is their absmean, solved by re-running the group
until the scale it uses is the absmean of the values it produces, which makes `fake_quant(latent)` give GPTQ's codes.
"""
from __future__ import annotations
import torch
from lowbit.quant.ternary import QuantSpec, init_scale, ternary_codes
def _codes(w: torch.Tensor, s: torch.Tensor) -> torch.Tensor:
"""Ternary codes of one column [E, R] against per-row scales s [E, R] (as `ternary_codes`, absmean)."""
return torch.sign(w) * (w.abs() > 0.5 * s)
def _inverse_cholesky_upper(H: torch.Tensor, damp: float, tries: int = 4) -> tuple[torch.Tensor, torch.Tensor, int]:
"""Upper Cholesky factor of H⁻¹ per batch element, in fp64. An element that is not positive-definite retries
with 10× the dampening; after `tries` it gets the identity (round-to-nearest, no compensation). Returns
(U, dampening used per element, number of elements left on the identity)."""
E, C, _ = H.shape
eye = torch.eye(C, dtype=H.dtype, device=H.device)
mean_diag = torch.diagonal(H, dim1=-2, dim2=-1).mean(-1)
U = eye.expand(E, C, C).clone()
used = torch.full((E,), float("nan"), dtype=H.dtype, device=H.device)
todo = torch.ones(E, dtype=torch.bool, device=H.device)
d = damp
for _ in range(tries):
Hd = H[todo] + (d * mean_diag[todo])[:, None, None] * eye
L, info = torch.linalg.cholesky_ex(Hd)
Uc, info2 = torch.linalg.cholesky_ex(torch.cholesky_inverse(L), upper=True)
good = (info == 0) & (info2 == 0) & torch.isfinite(Uc).flatten(1).all(-1)
idx = todo.nonzero()[:, 0]
U[idx[good]] = Uc[good]
used[idx[good]] = d
todo[idx[good]] = False
if not todo.any():
break
d *= 10
return U, used, int(todo.sum())
@torch.no_grad()
def gptq_ternary(w: torch.Tensor, h: torch.Tensor, spec: QuantSpec, damp: float = 0.01,
iters: int = 3) -> tuple[torch.Tensor, dict]:
"""w [E, R, C] (rows = outputs, C = inputs), h [E, C, C] → error-compensated latent [E, R, C] in fp32.
Batched over E (a layer's experts). Inputs with a zero Hessian diagonal (never active) are rounded like any
other column but pass no error on (their diagonal is set to 1).
"""
if spec.method != "absmean":
raise NotImplementedError("GPTQ init is built for Q-A (absmean) only")
g = spec.group_size
E, R, C = w.shape
W = w.detach().float().clone()
H = h.detach().double().clone() # fp64: real Hessians (massive activations in a few dims) break fp32 Cholesky
diag = torch.diagonal(H, dim1=-2, dim2=-1)
diag[diag == 0] = 1.0
U, damp_used, n_identity = _inverse_cholesky_upper(H, damp)
U = U.float()
latent = torch.empty_like(W)
used = torch.empty(E, R, C, dtype=torch.int8, device=W.device) # the codes GPTQ chose
passes = []
for g0 in range(0, C, g):
g1 = min(g0 + g, C)
start = W[:, :, g0:g1].clone()
s = init_scale(start, QuantSpec(method="absmean", group_size=g1 - g0)) # [E, R, 1]
for it in range(iters):
Wg = start.clone()
err = torch.empty_like(Wg)
for j in range(g1 - g0):
i = g0 + j
col = Wg[:, :, j]
c = _codes(col, s[..., 0])
used[:, :, i] = c.to(torch.int8)
q = c * s[..., 0]
e = (col - q) / U[:, i, i][:, None]
err[:, :, j] = e
if j + 1 < g1 - g0:
Wg[:, :, j + 1:] -= e[:, :, None] * U[:, i, i + 1:g1][:, None, :]
s_new = init_scale(Wg, QuantSpec(method="absmean", group_size=g1 - g0))
same = torch.equal(s_new, s)
s = s_new
if same:
break
passes.append(it + 1)
latent[:, :, g0:g1] = Wg
if g1 < C:
W[:, :, g1:] -= torch.bmm(err, U[:, g0:g1, g1:])
codes_latent = ternary_codes(latent, init_scale(latent, spec), spec)
return latent, {"group_passes_max": max(passes), "group_passes_mean": sum(passes) / len(passes),
"zero_fraction": float((codes_latent == 0).float().mean()),
"code_mismatch": float((codes_latent != used).float().mean()),
"damp_max": float(damp_used.nan_to_num(0).max()), "damp_raised": int((damp_used > damp).sum()),
"identity_fallback": n_identity}