jc-914's picture
Add files using upload-large-folder tool
2d2f883 verified
Raw History Blame Contribute Delete
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
@torch.no_grad()
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()