Angshul's picture
Upload 17 files
c675b8d verified
Raw History Blame Contribute Delete
5.7 kB
"""Quantizer core (torch, CPU or GPU).
Grid: standard asymmetric GPTQ grid per (output row, input group): range includes 0,
scale = (max - min) / (2^b - 1), integer zero-point z = round(-min / scale), q = clamp(round(w/scale) + z, 0, 2^b - 1),
w_hat = (q - z) * scale. Grids are fixed from the original weights ("static groups").
Zero-points are kept >= 1 so the checkpoint can use the GPTQ v1 storage convention (stores z - 1).
Rounding: sequential error feedback that minimises tr(R H R^T) for a design matrix H (in x in), i.e. the
OBQ/GPTQ recursion with lazy block updates. H = None gives round-to-nearest. With H = I it is identical to RTN.
"""
import torch
def group_params(W, bits, group):
"""W: (out, in). Returns scale, zero of shape (out, in // group)."""
out, inn = W.shape
assert inn % group == 0, f"input dim {inn} not divisible by group size {group}"
maxq = 2 ** bits - 1
Wg = W.reshape(out, inn // group, group)
xmin = torch.clamp(Wg.amin(-1), max=0.0)
xmax = torch.clamp(Wg.amax(-1), min=0.0)
both0 = (xmin == 0) & (xmax == 0)
xmin, xmax = torch.where(both0, -torch.ones_like(xmin), xmin), torch.where(both0, torch.ones_like(xmax), xmax)
scale = (xmax - xmin) / maxq
zero = torch.round(-xmin / scale)
# keep z >= 1 (GPTQ v1 storage): if the group has no negative part, use z = 1 and rescale so max still fits
low = zero < 1
scale = torch.where(low, xmax / (maxq - 1), scale)
zero = torch.where(low, torch.ones_like(zero), zero)
zero = torch.clamp(zero, 1, maxq)
return scale, zero
def _q(w, scale, zero, maxq):
return torch.clamp(torch.round(w / scale) + zero, 0, maxq)
def quantize(W, bits, group, H=None, block=128, dynamic_groups=True):
"""W: (out, in) float tensor (torch Linear layout). Returns (q uint8 (out, in), scale (out, G), zero (out, G)).
H: (in, in) positive-definite design matrix, or None for round-to-nearest.
dynamic_groups: each group's grid is fitted to the error-fed (updated) weights when the group starts,
as in standard GPTQ, so feedback never pushes weights outside their grid. False = grids from the original W."""
W = W.to(torch.float32)
out, inn = W.shape
maxq = 2 ** bits - 1
scale, zero = group_params(W, bits, group)
gid = torch.arange(inn, device=W.device) // group
if H is None:
Q = _q(W, scale[:, gid], zero[:, gid], maxq)
return Q.to(torch.uint8), scale, zero
W = W.clone()
H = H.to(device=W.device, dtype=torch.float32)
L = torch.linalg.cholesky(H)
Hinv = torch.cholesky_inverse(L); del L
U = torch.linalg.cholesky(Hinv, upper=True); del Hinv
Q = torch.zeros((out, inn), dtype=torch.uint8, device=W.device)
for i1 in range(0, inn, block):
i2 = min(i1 + block, inn); n = i2 - i1
W1 = W[:, i1:i2].clone(); E1 = torch.zeros_like(W1); U1 = U[i1:i2, i1:i2]
for i in range(n):
c = i1 + i; g = c // group
if dynamic_groups and c % group == 0:
seg = W1[:, i:i + group] if i + group <= n else torch.cat([W1[:, i:], W[:, i2:c + group]], 1)
sg, zg = group_params(seg, bits, group)
scale[:, g], zero[:, g] = sg[:, 0], zg[:, 0]
w = W1[:, i]
q = _q(w, scale[:, g], zero[:, g], maxq)
Q[:, c] = q.to(torch.uint8)
err = (w - (q - zero[:, g]) * scale[:, g]) / U1[i, i]
W1[:, i:] -= err.unsqueeze(1) * U1[i, i:].unsqueeze(0)
E1[:, i] = err
if i2 < inn:
W[:, i2:] -= E1 @ U[i1:i2, i2:]
return Q, scale, zero
def dequantize(Q, scale, zero, group):
gid = torch.arange(Q.shape[1], device=Q.device) // group
return (Q.to(torch.float32) - zero[:, gid]) * scale[:, gid]
# --------------------------------------------------------------- GPTQ v1 packing (AutoGPTQ / GPTQModel "gptq" format)
def pack(Q, scale, zero, bits, group):
"""Returns dict(qweight int32 (in*bits/32, out), qzeros int32 (G, out*bits/32), scales fp16 (G, out), g_idx int32 (in,))."""
assert bits in (2, 4, 8)
per = 32 // bits
out, inn = Q.shape
q = Q.t().contiguous().to(torch.int64) # (in, out)
assert inn % per == 0 and out % per == 0
qw = torch.zeros((inn // per, out), dtype=torch.int64, device=Q.device)
for k in range(per):
qw |= q[k::per, :] << (bits * k)
z = (zero.t().contiguous().to(torch.int64) - 1) # v1 convention: store z - 1
qz = torch.zeros((z.shape[0], out // per), dtype=torch.int64, device=Q.device)
for k in range(per):
qz |= z[:, k::per] << (bits * k)
to32 = lambda t: (t & 0xFFFFFFFF).to(torch.int64).where(t < 2 ** 31, t - 2 ** 32).to(torch.int32)
return {"qweight": to32(qw), "qzeros": to32(qz), "scales": scale.t().contiguous().to(torch.float16),
"g_idx": (torch.arange(inn, device=Q.device) // group).to(torch.int32)}
def unpack(qweight, qzeros, scales, g_idx, bits):
"""Inverse of pack: dequantized weight in torch Linear layout (out, in), float32."""
per = 32 // bits; mask = 2 ** bits - 1
qw = qweight.to(torch.int64) & 0xFFFFFFFF
inn = qw.shape[0] * per
q = torch.empty((inn, qw.shape[1]), dtype=torch.int64, device=qw.device)
for k in range(per):
q[k::per] = (qw >> (bits * k)) & mask
qz = qzeros.to(torch.int64) & 0xFFFFFFFF
z = torch.empty((qz.shape[0], qz.shape[1] * per), dtype=torch.int64, device=qz.device)
for k in range(per):
z[:, k::per] = ((qz >> (bits * k)) & mask) + 1
g = g_idx.to(torch.int64)
W = (q.to(torch.float32) - z[g].to(torch.float32)) * scales[g].to(torch.float32) # (in, out)
return W.t().contiguous()