Download quantization/quantize.py from jc-914/prism-quantized: direct link, hf CLI and curl.
- Browser
- Download file 12.6 kB
-
https://huggingface.co/jc-914/prism-quantized/resolve/main/quantization/quantize.py
- Command line
-
hf download hf://jc-914/prism-quantized/quantization/quantize.py
-
curl -L -o quantize.py https://huggingface.co/jc-914/prism-quantized/resolve/main/quantization/quantize.py
12.6 kB
| """Sarvam-Prism -> INT4 package for <=16 GB GPUs: routed experts to INT4 (GPTQ, group 128, symmetric), everything else kept as shipped. | |
| Stages (one process; calibration activations live in RAM): | |
| 1. load the original FP8 model on the big GPU, run the evaluation suite -> ref.json | |
| 2. run calibration requests, record each MoE layer's input tokens (and their routing) | |
| 3. free the GPU; for each MoE layer: build per-expert Hessians, GPTQ all 128 experts batched on the GPU, | |
| pack to int4, write the output layer file, delete the source layer file (space on /dev/shm is limited) | |
| """ | |
| import gc, json, os, shutil, sys, time | |
| import torch | |
| import torch.nn.functional as F | |
| from safetensors import safe_open | |
| from safetensors.torch import save_file | |
| SRC, OUT, WORK = "/dev/shm/src", "/dev/shm/out", "/workspace/work" | |
| GROUP, QMAX, QMIN = 128, 7, -8 | |
| CALIB_TOKENS = int(os.environ.get("CALIB_TOKENS", 262144)) | |
| CLIPS = torch.tensor([1.0, 0.96, 0.92, 0.88, 0.84, 0.80]) | |
| os.makedirs(OUT, exist_ok=True); os.makedirs(WORK, exist_ok=True) | |
| log = lambda *a: print(time.strftime("%H:%M:%S"), *a, flush=True) | |
| # ------------------------------------------------------------------------------------------------ stage 1 + 2 | |
| def reference_and_calibrate(): | |
| sys.path.insert(0, SRC); sys.path.insert(0, os.path.dirname(__file__)); sys.path.insert(0, WORK) | |
| import prism | |
| from suite import run | |
| from calib_requests import make_requests | |
| t = time.time() | |
| model = prism.Prism.from_pretrained(SRC, cuda_graphs=False) | |
| log(f"reference loaded in {time.time()-t:.0f}s, GPU mem {torch.cuda.memory_allocated()/1e9:.1f} GB") | |
| if not os.path.exists(f"{WORK}/ref.json"): | |
| run(model, f"{WORK}/ref.json"); log("reference suite done") | |
| store, state = {}, {"on": False} | |
| for name, m in model.backbone.named_modules(): | |
| if type(m).__name__ == "SarvamMLAMoE": | |
| li = int(name.split(".")[1]); orig = m._moe_dispatch; store[li] = ([], []) | |
| def wrap(x, ids, w, li=li, orig=orig): | |
| if state["on"]: | |
| lens = prism.SPLIT["len"]; B = len(lens); L = x.shape[0] // B | |
| sel = torch.cat([torch.arange(b * L, b * L + n, device=x.device) for b, n in enumerate(lens)]) | |
| store[li][0].append(x[sel].to(torch.bfloat16).cpu()); store[li][1].append(ids[sel].to(torch.uint8).cpu()) | |
| return orig(x, ids, w) | |
| m._moe_dispatch = wrap | |
| reqs = make_requests(os.path.join(os.path.dirname(__file__), "wiki.json")) | |
| state["on"] = True; n_tok, t = 0, time.time() | |
| for i, (s, q) in enumerate(reqs): | |
| model.decide(state=s, questions=q) | |
| n_tok = sum(x.shape[0] for x in store[1][0]) | |
| if i % 200 == 0: log(f"calib req {i} tokens {n_tok}") | |
| if n_tok >= CALIB_TOKENS: break | |
| state["on"] = False | |
| log(f"calibration: {i+1} requests, {n_tok} tokens, {time.time()-t:.0f}s") | |
| calib = {li: (torch.cat(xs), torch.cat(ids)) for li, (xs, ids) in store.items()} | |
| del model, store; gc.collect(); torch.cuda.empty_cache() | |
| return calib | |
| # ------------------------------------------------------------------------------------------------ GPTQ | |
| def group_scale(w): | |
| """w: [E, N, G] -> per-(E,N) scale chosen from CLIPS by minimum rounding MSE.""" | |
| amax = w.abs().amax(-1).clamp_min(1e-8) | |
| best, best_err = None, None | |
| for c in CLIPS.tolist(): | |
| s = amax * c / QMAX | |
| err = ((torch.clamp(torch.round(w / s[..., None]), QMIN, QMAX) * s[..., None] - w) ** 2).sum(-1) | |
| if best is None: | |
| best, best_err = s, err | |
| else: | |
| m = err < best_err; best = torch.where(m, s, best); best_err = torch.where(m, err, best_err) | |
| return best | |
| def gptq(W, H, cnt, damp=0.01, min_tokens=512): | |
| """Batched GPTQ. W: [E, N, K] fp32, H: [E, K, K] fp32 (sum of x x^T), cnt: calibration tokens per expert. | |
| Experts seen by fewer than min_tokens tokens get H = I (plain round-to-nearest with clip search: no error | |
| feedback fitted to a handful of samples). Inputs never seen (zero diagonal) keep their weights (diag set to the | |
| mean) so rare inputs at deployment are still served. Returns int q [E,N,K] and fp32 scales [E,N,K/G].""" | |
| E, N, K = W.shape | |
| W = W.clone(); H = H.clone() | |
| few = (cnt < min_tokens).to(H.device) | |
| if few.any(): | |
| H[few] = torch.eye(K, device=H.device) | |
| diag = torch.diagonal(H, dim1=1, dim2=2) | |
| mean = diag.mean(-1, keepdim=True).clamp_min(1e-8) | |
| dead = diag <= 0 | |
| diag.copy_(torch.where(dead, mean.expand_as(diag), diag)) | |
| diag += damp * mean | |
| L, info = torch.linalg.cholesky_ex(H) | |
| if info.any(): # extra damping for ill-conditioned experts | |
| bad = info.nonzero().flatten() | |
| for e in bad.tolist(): | |
| d = torch.diagonal(H[e]); d += 0.1 * d.mean() | |
| L, info = torch.linalg.cholesky_ex(H) | |
| assert not info.any(), "Cholesky failed after extra damping" | |
| Hinv = torch.cholesky_inverse(L) | |
| Hinv = torch.linalg.cholesky(Hinv, upper=True) | |
| Q = torch.empty(E, N, K, dtype=torch.int8, device=W.device) | |
| S = torch.empty(E, N, K // GROUP, dtype=torch.float32, device=W.device) | |
| for g, i1 in enumerate(range(0, K, GROUP)): | |
| i2 = i1 + GROUP | |
| W1 = W[:, :, i1:i2].clone(); Err = torch.empty_like(W1); Hi = Hinv[:, i1:i2, i1:i2] | |
| s = group_scale(W1); S[:, :, g] = s | |
| for i in range(GROUP): | |
| w = W1[:, :, i] | |
| q = torch.clamp(torch.round(w / s), QMIN, QMAX) | |
| Q[:, :, i1 + i] = q.to(torch.int8) | |
| err = (w - q * s) / Hi[:, i, i][:, None] | |
| W1[:, :, i:] -= err[:, :, None] * Hi[:, i, i:][:, None, :] | |
| Err[:, :, i] = err | |
| if i2 < K: | |
| W[:, :, i2:] -= torch.bmm(Err, Hinv[:, i1:i2, i2:]) | |
| return Q, S | |
| def rtn(W): | |
| E, N, K = W.shape | |
| Wg = W.view(E, N, K // GROUP, GROUP) | |
| s = Wg.abs().amax(-1).clamp_min(1e-8) / QMAX | |
| return torch.clamp(torch.round(Wg / s[..., None]), QMIN, QMAX).view(E, N, K).to(torch.int8), s | |
| def dequant(Q, S): | |
| E, N, K = Q.shape | |
| return (Q.float().view(E, N, K // GROUP, GROUP) * S[..., None]).view(E, N, K) | |
| def pack(Q): | |
| """int q in [-8,7], [E,N,K] -> uint8 [E,N,K/2]. Within each 128-group, byte j holds element j (low nibble) and | |
| element j+64 (high nibble).""" | |
| E, N, K = Q.shape | |
| u = (Q.to(torch.int16) + 8).to(torch.uint8).view(E, N, K // GROUP, 2, GROUP // 2) | |
| return (u[:, :, :, 0] | (u[:, :, :, 1] << 4)).reshape(E, N, K // 2).contiguous() | |
| def unpack(P): | |
| E, N, K2 = P.shape | |
| p = P.view(E, N, K2 // (GROUP // 2), GROUP // 2) | |
| lo, hi = (p & 15).to(torch.int8) - 8, (p >> 4).to(torch.int8) - 8 | |
| return torch.stack([lo, hi], 3).reshape(E, N, K2 * 2) | |
| def hessians(X, ids, n_exp, fn=None, dim=None): | |
| """Per-expert sum x x^T over the tokens routed to it. fn(e, x) maps the token inputs (e.g. to the down-proj | |
| input). Returns H [E, D, D] fp32 and token counts.""" | |
| flat = ids.long() | |
| D = dim or X.shape[1] | |
| H = torch.zeros(n_exp, D, D, device="cuda") | |
| cnt = torch.zeros(n_exp, dtype=torch.long) | |
| tok = torch.arange(X.shape[0], device=X.device).repeat_interleave(ids.shape[1]) | |
| order = flat.flatten().argsort() | |
| e_sorted, t_sorted = flat.flatten()[order], tok[order] | |
| bounds = torch.searchsorted(e_sorted, torch.arange(n_exp + 1, device=X.device)).tolist() | |
| for e in range(n_exp): | |
| t = t_sorted[bounds[e]:bounds[e + 1]] | |
| cnt[e] = t.numel() | |
| for c in range(0, t.numel(), 32768): | |
| x = X[t[c:c + 32768]].float() | |
| if fn is not None: x = fn(e, x) | |
| H[e].addmm_(x.T, x) | |
| return H, cnt | |
| def rel_err(Wq, W, H): | |
| """Hessian-weighted relative output error: tr(D H D^T) / tr(W H W^T), averaged over experts.""" | |
| D = Wq - W | |
| num = torch.einsum("enk,ekj,enj->e", D, H, D); den = torch.einsum("enk,ekj,enj->e", W, H, W) | |
| return (num / den.clamp_min(1e-12)).mean().item() | |
| # ------------------------------------------------------------------------------------------------ stage 3 | |
| def quantize_layer(li, X, ids, stats): | |
| f = f"model-layer-{li:02d}.safetensors" | |
| tensors = {} | |
| with safe_open(f"{SRC}/{f}", "pt", device="cuda") as sf: | |
| for k in sf.keys(): tensors[k] = sf.get_tensor(k) | |
| p = f"model.layers.{li}.mlp.experts." | |
| gu = tensors.pop(p + "gate_up_proj").float() * tensors.pop(p + "gate_up_proj_scale")[..., None] # [E, 2I, H] | |
| dn = tensors.pop(p + "down_proj").float() * tensors.pop(p + "down_proj_scale")[..., None] # [E, H, I] | |
| E, I2, Hd = gu.shape; I = I2 // 2 | |
| X, ids = X.cuda(), ids.cuda() | |
| t = time.time() | |
| Hgu, cnt = hessians(X, ids, E) | |
| Qgu, Sgu = gptq(gu, Hgu, cnt) | |
| guq = dequant(Qgu, Sgu) | |
| e_gu = rel_err(guq, gu, Hgu); e_gu_rtn = rel_err(dequant(*rtn(gu)), gu, Hgu); del Hgu | |
| def act(e, x): # down-proj input through the *quantized* gate/up | |
| h = x @ guq[e].T | |
| return F.silu(h[:, :I]) * h[:, I:] | |
| Hdn, _ = hessians(X, ids, E, fn=act, dim=I) | |
| Qdn, Sdn = gptq(dn, Hdn, cnt) | |
| e_dn = rel_err(dequant(Qdn, Sdn), dn, Hdn); e_dn_rtn = rel_err(dequant(*rtn(dn)), dn, Hdn); del Hdn | |
| assert torch.equal(unpack(pack(Qgu)), Qgu) and torch.equal(unpack(pack(Qdn)), Qdn) | |
| tensors[p + "gate_up_qweight"] = pack(Qgu).cpu(); tensors[p + "gate_up_scales"] = Sgu.float().cpu() | |
| tensors[p + "down_qweight"] = pack(Qdn).cpu(); tensors[p + "down_scales"] = Sdn.float().cpu() | |
| assert torch.isfinite(Sgu).all() and (Sgu > 0).all() and torch.isfinite(Sdn).all() and (Sdn > 0).all() | |
| tensors = {k: v.cpu().contiguous() for k, v in tensors.items()} | |
| save_file(tensors, f"{OUT}/{f}.tmp", metadata={"format": "pt"}) | |
| os.replace(f"{OUT}/{f}.tmp", f"{OUT}/{f}") | |
| stats[li] = dict(gate_up_gptq=e_gu, gate_up_rtn=e_gu_rtn, down_gptq=e_dn, down_rtn=e_dn_rtn, | |
| min_tokens=int(cnt.min()), rtn_experts=int((cnt < 512).sum()), median_tokens=int(cnt.median()), seconds=round(time.time() - t, 1)) | |
| log(f"layer {li}: {stats[li]}") | |
| os.remove(f"{SRC}/{f}") | |
| return list(tensors) | |
| def main(): | |
| stats_path = f"{WORK}/quant_stats.json" | |
| stats = json.load(open(stats_path)) if os.path.exists(stats_path) else {} | |
| stats = {int(k): v for k, v in stats.items()} | |
| calib = reference_and_calibrate() | |
| failed = [] | |
| cfg = json.load(open(f"{SRC}/config.json")) | |
| for li in range(cfg["first_k_dense_replace"], cfg["num_hidden_layers"]): | |
| if os.path.exists(f"{OUT}/model-layer-{li:02d}.safetensors"): | |
| continue | |
| X, ids = calib.pop(li) | |
| try: | |
| quantize_layer(li, X, ids, stats) | |
| except Exception: | |
| import traceback; traceback.print_exc() | |
| log(f"layer {li} FAILED; continuing (rerun to retry)"); failed.append(li) | |
| json.dump(stats, open(stats_path, "w"), indent=1) | |
| del X, ids; gc.collect(); torch.cuda.empty_cache() | |
| if failed: | |
| log(f"FAILED layers: {failed}"); sys.exit(1) | |
| # everything else is copied unchanged | |
| for f in os.listdir(SRC): | |
| if f.startswith(".") or f in ("prism.py", "example.py", "requirements.txt", "README.md", "model.safetensors.index.json"): | |
| continue | |
| if not os.path.exists(f"{OUT}/{f}"): | |
| (shutil.copytree if os.path.isdir(f"{SRC}/{f}") else shutil.copy2)(f"{SRC}/{f}", f"{OUT}/{f}") | |
| wm = {} | |
| for f in sorted(os.listdir(OUT)): | |
| if f.endswith(".safetensors") and f.startswith("model-"): | |
| with safe_open(f"{OUT}/{f}", "pt") as sf: | |
| for k in sf.keys(): wm[k] = f | |
| json.dump({"metadata": {}, "weight_map": wm}, open(f"{OUT}/model.safetensors.index.json", "w"), indent=1) | |
| cfg["quantization"] = { | |
| "routed_experts": {"format": "int4", "method": "GPTQ (damp 0.01, MSE clip search)", "symmetric": True, | |
| "group_size": GROUP, "scale_dtype": "float32", | |
| "packing": "uint8 [E, N, K/2]; in each 128-group byte j = elem j (low nibble) | elem j+64 (high nibble); value = nibble - 8", | |
| "calibration": f"{CALIB_TOKENS} tokens of Prism-format requests over multilingual Wikipedia (11 languages)"}, | |
| "other_projections": {"format": "float8_e4m3fn", "weight_scale": "per output row (absmax / 448)", "note": "unchanged from sarvamai/prism"}, | |
| "embeddings_norms": "bfloat16", "router": "float32"} | |
| json.dump(cfg, open(f"{OUT}/config.json", "w"), indent=2) | |
| log("done") | |
| if __name__ == "__main__": | |
| main() | |