Download shapequant/core.py from Angshul/Shapequant: direct link, hf CLI and curl.
- Browser
- Download file 5.7 kB
-
https://huggingface.co/Angshul/Shapequant/resolve/main/shapequant/core.py
- Command line
-
hf download hf://Angshul/Shapequant/shapequant/core.py
-
curl -L -o core.py https://huggingface.co/Angshul/Shapequant/resolve/main/shapequant/core.py
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() | |