traj-mc / code /compress /compress_baselines.py
ttishere's picture
Publish code/compress
9ad45b8 verified
Raw History Blame Contribute Delete
19.9 kB
"""compress_baselines.py -- pruning / quantization baselines on LLaDA-8B-Base.
Methods: wanda, sparsegpt (pruning), gptq, awq (weight-only FAKE quant: quantize ->
dequantize -> fp16). Each nn.Linear -> a dense bf16 weight saved as {prefix}_weight.pt
(loaded by eval/llada_harness.py replace_with_decoded_weights -- auto-detected since no
*_A.pt is present). Uses the SAME calib .pt as the SVD arms (base/ours/asvd) so all
methods are calibration-matched. X^T X (second moment of inputs) is collected in one
forward pass, GPU-resident and memory-binned, offloaded to CPU per bin (like compress.py).
Pruning sparsity default 0.23 == keep 0.77 == the model_ratio 0.8 retention of the SVD arms.
"""
import os, sys, time, argparse
import torch
import torch.nn as nn
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import common as C
# ── calib X^T X collection (GPU-resident, binned) β€” same as compress.py ──────────
# NB Traj-MC fix: with the noised (ours) calib ~47% of positions are MASK_ID, whose
# input embedding is a single degenerate vector. Accumulating those rows into X^T X
# injects a giant rank-1 spike (breaks the cholesky-inverse error comp in
# gptq/sparsegpt) and washes out the per-channel outlier signal (wanda/awq under-
# respond). The calib DATA is the full trajectory (t~U[0,1], t=1 full-mask kept) β€”
# we only restrict the Monte-Carlo *statistic* to the real-token rows, i.e. the
# activations the compressed layer actually has to preserve. mask_id=None (clean
# base calib, 0% mask) is a no-op, so the base arm is byte-for-byte unchanged.
def collect_xtx_cpu(model, input_ids, target_mods, device, log_prefix="", mask_id=None):
"""Legacy path: per-sample D2H copy + CPU accumulate (~33s/sample on A5000).
Kept only for --xtx_cpu reproduction; collect_xtx_gpu computes the identical
statistic on GPU."""
cov, ntok = {}, {}
_keep = {"m": None} # per-sample real-token row mask, shape [L], set before each fwd
def make_hook(name, in_dim):
def hook(module, inp, out):
x = inp[0].detach().reshape(-1, in_dim).float()
k = _keep["m"]
if k is not None and k.numel() == x.shape[0]:
x = x[k] # drop MASK-position rows
if x.shape[0] == 0: # fully-masked sample -> contributes nothing
return
if name not in cov:
cov[name] = torch.zeros(in_dim, in_dim, dtype=torch.float32) # CPU 累加,GPU εͺζ”Ύζ¨‘εž‹
ntok[name] = 0
cov[name] += (x.t() @ x).cpu()
ntok[name] += x.shape[0]
return hook
handles = [m.register_forward_hook(make_hook(n, m.in_features))
for n, m in target_mods.items()]
t0 = time.time()
with torch.no_grad():
for i in range(input_ids.shape[0]):
ids = input_ids[i:i + 1].to(device)
_keep["m"] = None if mask_id is None else (ids.reshape(-1) != mask_id)
model(ids)
torch.cuda.empty_cache()
if i % 200 == 0:
print(f" {log_prefix}collect {i}/{input_ids.shape[0]} ({time.time()-t0:.0f}s)", flush=True)
for h in handles:
h.remove()
out = {n: (cov[n] / ntok[n]) for n in list(cov)}
for n in list(cov):
cov[n] = None
torch.cuda.empty_cache()
return out
def collect_xtx_gpu(model, input_ids, target_mods, device, log_prefix="", mask_id=None):
"""Same statistic as collect_xtx_cpu (incl. the real-token row restriction), but
accumulated ON GPU via addmm_ (no per-forward D2H copy), offloaded to CPU once at
the end β€” ported from compress.py. Caller must ensure this group's total XtX
(sum of in_features^2 * 4 bytes) fits on the GPU beside the model (use plan_bins)."""
cov, ntok = {}, {}
_keep = {"m": None} # per-sample real-token row mask, shape [L], set before each fwd
def make_hook(name, in_dim):
def hook(module, inp, out):
x = inp[0].detach().reshape(-1, in_dim).float()
k = _keep["m"]
if k is not None and k.numel() == x.shape[0]:
x = x[k] # drop MASK-position rows
if x.shape[0] == 0: # fully-masked sample -> contributes nothing
return
if name not in cov:
cov[name] = torch.zeros(in_dim, in_dim, dtype=torch.float32, device=device)
ntok[name] = 0
cov[name].addmm_(x.t(), x) # cov += x^T x, in-place on GPU
ntok[name] += x.shape[0]
return hook
handles = [m.register_forward_hook(make_hook(n, m.in_features))
for n, m in target_mods.items()]
t0 = time.time()
with torch.no_grad():
for i in range(input_ids.shape[0]):
ids = input_ids[i:i + 1].to(device)
_keep["m"] = None if mask_id is None else (ids.reshape(-1) != mask_id)
model(ids)
if i % 200 == 0:
print(f" {log_prefix}collect {i}/{input_ids.shape[0]} ({time.time()-t0:.0f}s)", flush=True)
for h in handles:
h.remove()
out = {}
for n in list(cov):
out[n] = (cov[n] / ntok[n]).cpu()
cov[n] = None
del cov
torch.cuda.empty_cache()
return out
def plan_bins(targets, budget_bytes):
bins, cur, cur_b = [], {}, 0
for name, mod in targets:
b = mod.in_features * mod.in_features * 4
if cur and cur_b + b > budget_bytes:
bins.append(cur); cur, cur_b = {}, 0
cur[name] = mod; cur_b += b
if cur:
bins.append(cur)
return bins
# ── methods (each returns a dense W' of the SAME shape) ──────────────────────────
def wanda_prune(W, XtX, sparsity, prune_n=2, prune_m=4):
"""Wanda: importance = |W| * ||X_col||.
prune_n:prune_m = N:M STRUCTURED sparsity (default 2:4 = zero the 2 lowest of every
4 consecutive input cols -> 50% sparse, hardware-accelerated). prune_n=0 falls back
to UNSTRUCTURED at `sparsity` (no real speedup)."""
scaler = torch.sqrt(torch.diag(XtX).clamp(min=0)).reshape(1, -1) # [1, in]
metric = W.abs() * scaler
W = W.clone()
if prune_n > 0:
out, inc = W.shape
g = inc // prune_m
mt = metric[:, :g * prune_m].reshape(out, g, prune_m)
idx = mt.topk(prune_n, dim=2, largest=False).indices # 2 lowest per group of 4
mask = torch.zeros_like(mt, dtype=torch.bool).scatter_(2, idx, True).reshape(out, g * prune_m)
Wv = W[:, :g * prune_m]
Wv[mask] = 0.0
W[:, :g * prune_m] = Wv
else:
k = int(W.shape[1] * sparsity)
if k > 0:
idx = torch.topk(metric, k, dim=1, largest=False).indices
W.scatter_(1, idx, 0.0)
return W
def sparsegpt_prune(W, H, sparsity, blocksize=128, percdamp=0.01, prune_n=2, prune_m=4):
"""Faithful port of SparseGPT fasterprune (Sink-Aware-Pruning/lib/sparsegpt.py).
H = X X^T (== our XtX). Column-wise pruning with Hessian error compensation.
prune_n:prune_m = N:M STRUCTURED (default 2:4, hardware-accelerated). prune_n=0 ->
UNSTRUCTURED at `sparsity`."""
W = W.clone().float()
H = H.clone().float()
cols = W.shape[1]
dead = torch.diag(H) == 0
H[dead, dead] = 1.0
W[:, dead] = 0.0
damp = percdamp * torch.mean(torch.diag(H))
diag = torch.arange(cols, device=W.device)
H[diag, diag] += damp
H = torch.linalg.cholesky(H)
H = torch.cholesky_inverse(H)
H = torch.linalg.cholesky(H, upper=True)
Hinv = H
for i1 in range(0, cols, blocksize):
i2 = min(i1 + blocksize, cols)
count = i2 - i1
W1 = W[:, i1:i2].clone()
Q1 = torch.zeros_like(W1)
Err1 = torch.zeros_like(W1)
Hinv1 = Hinv[i1:i2, i1:i2]
mask1 = torch.zeros_like(W1, dtype=torch.bool)
if prune_n == 0: # unstructured: one threshold over the block
tmp = W1 ** 2 / (torch.diag(Hinv1).reshape((1, -1))) ** 2
thresh = torch.sort(tmp.flatten())[0][int(tmp.numel() * sparsity)]
mask1 = tmp <= thresh
for i in range(count):
w = W1[:, i]
d = Hinv1[i, i]
if prune_n != 0 and i % prune_m == 0: # N:M: pick prune_n lowest of the next prune_m cols
tmp = W1[:, i:i + prune_m] ** 2 \
/ (torch.diag(Hinv1)[i:i + prune_m].reshape((1, -1))) ** 2
mask1.scatter_(1, i + torch.topk(tmp, prune_n, dim=1, largest=False).indices, True)
q = w.clone()
q[mask1[:, i]] = 0.0
Q1[:, i] = q
err1 = (w - q) / d
W1[:, i:] -= err1.unsqueeze(1).matmul(Hinv1[i, i:].unsqueeze(0))
Err1[:, i] = err1
W[:, i1:i2] = Q1
W[:, i2:] -= Err1.matmul(Hinv[i1:i2, i2:])
return W
# ── GPTQ (IST-DASLab/gptq) weight-only fake-quant, asym per-row per-group ────────
def _gptq_find_params(W, bits, sym=False):
maxq = 2 ** bits - 1
x = W.flatten(1)
zeros = torch.zeros(x.shape[0], device=x.device)
xmin = torch.minimum(x.min(1)[0], zeros)
xmax = torch.maximum(x.max(1)[0], zeros)
if sym:
xmax = torch.maximum(xmin.abs(), xmax)
neg = xmin < 0
xmin[neg] = -xmax[neg]
deg = (xmin == 0) & (xmax == 0)
xmin[deg], xmax[deg] = -1, 1
scale = (xmax - xmin) / maxq
zero = torch.full_like(scale, (maxq + 1) / 2) if sym else torch.round(-xmin / scale)
return scale.unsqueeze(1), zero.unsqueeze(1), maxq
def _gptq_quant(x, scale, zero, maxq):
q = torch.clamp(torch.round(x / scale) + zero, 0, maxq)
return scale * (q - zero)
@torch.no_grad()
def gptq_quantize(W, H, bits=3, groupsize=128, percdamp=0.01, blocksize=128, sym=False, actorder=False):
orig_dtype = W.dtype
W = W.clone().float()
H = H.clone().float()
rows, columns = W.shape
dev = W.device
dead = torch.diag(H) == 0
H[dead, dead] = 1
W[:, dead] = 0
invperm = None
if actorder:
perm = torch.argsort(torch.diag(H), descending=True)
W = W[:, perm]; H = H[perm][:, perm]
invperm = torch.argsort(perm)
if groupsize == -1:
scale, zero, maxq = _gptq_find_params(W, bits, sym)
else:
maxq = 2 ** bits - 1
scale = zero = None
Q = torch.zeros_like(W)
damp = percdamp * torch.mean(torch.diag(H))
diag = torch.arange(columns, device=dev)
H[diag, diag] += damp
H = torch.linalg.cholesky(H)
H = torch.cholesky_inverse(H)
Hinv = torch.linalg.cholesky(H, upper=True)
for i1 in range(0, columns, blocksize):
i2 = min(i1 + blocksize, columns)
count = i2 - i1
W1 = W[:, i1:i2].clone()
Q1 = torch.zeros_like(W1)
Err1 = torch.zeros_like(W1)
Hinv1 = Hinv[i1:i2, i1:i2]
for i in range(count):
w = W1[:, i]
d = Hinv1[i, i]
if groupsize != -1 and (i1 + i) % groupsize == 0:
scale, zero, _ = _gptq_find_params(W[:, (i1 + i):(i1 + i + groupsize)], bits, sym)
q = _gptq_quant(w.unsqueeze(1), scale, zero, maxq).flatten()
Q1[:, i] = q
err1 = (w - q) / d
W1[:, i:] -= err1.unsqueeze(1).matmul(Hinv1[i, i:].unsqueeze(0))
Err1[:, i] = err1
Q[:, i1:i2] = Q1
W[:, i2:] -= Err1.matmul(Hinv[i1:i2, i2:])
if actorder:
Q = Q[:, invperm]
return Q.to(orig_dtype)
# ── AWQ (mit-han-lab/llm-awq) activation-aware fake-quant, asym group-wise ────────
def _awq_pseudo_quant(w, n_bit=4, group=128, zero_point=True):
shape = w.shape
if group > 0:
assert shape[-1] % group == 0
w = w.reshape(-1, group)
if zero_point:
max_val = w.amax(dim=1, keepdim=True)
min_val = w.amin(dim=1, keepdim=True)
max_int, min_int = 2 ** n_bit - 1, 0
scales = (max_val - min_val).clamp(min=1e-5) / max_int
zeros = (-torch.round(min_val / scales)).clamp_(min_int, max_int)
w = (torch.clamp(torch.round(w / scales) + zeros, min_int, max_int) - zeros) * scales
else:
max_val = w.abs().amax(dim=1, keepdim=True).clamp(min=1e-5)
max_int, min_int = 2 ** (n_bit - 1) - 1, -(2 ** (n_bit - 1))
scales = max_val / max_int
w = torch.clamp(torch.round(w / scales), min_int, max_int) * scales
return w.reshape(shape)
@torch.no_grad()
def awq_quantize(W, act_scale, bits=4, groupsize=128, n_grid=20, zero_point=True, H=None, do_clip=True):
orig_dtype = W.dtype
W = W.clone().float()
act_scale = act_scale.float().to(W.device)
def loss(Wq):
d = W - Wq
if H is not None:
return torch.einsum('oi,ij,oj->', d, H.float(), d).item()
return (d.pow(2) * act_scale.pow(2).unsqueeze(0)).sum().item()
best_loss, best_s = float('inf'), torch.ones_like(act_scale)
for i in range(n_grid):
ratio = i / n_grid
s = act_scale.pow(ratio).clamp(min=1e-4)
s = s / (s.max() * s.min()).sqrt()
Wq = _awq_pseudo_quant(W * s.unsqueeze(0), bits, groupsize, zero_point) / s.unsqueeze(0)
L = loss(Wq)
if L < best_loss:
best_loss, best_s = L, s
Ws = W * best_s.unsqueeze(0)
if do_clip:
g = groupsize if groupsize > 0 else Ws.shape[1]
wg = Ws.reshape(Ws.shape[0], -1, g)
org_max = wg.abs().amax(dim=-1, keepdim=True)
best_max = org_max.clone()
best_err = torch.full_like(org_max, float('inf'))
for i in range(int(0.5 * n_grid)):
mx = org_max * (1 - i / n_grid)
q = _awq_pseudo_quant(torch.clamp(wg, -mx, mx).reshape(-1, g), bits, g, zero_point).reshape(wg.shape)
err = (q - wg).pow(2).mean(dim=-1, keepdim=True)
better = err < best_err
best_err = torch.where(better, err, best_err)
best_max = torch.where(better, mx, best_max)
Ws = torch.clamp(wg, -best_max, best_max).reshape(Ws.shape)
Wq = _awq_pseudo_quant(Ws, bits, groupsize, zero_point) / best_s.unsqueeze(0)
return Wq.to(orig_dtype)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--calib", required=True)
ap.add_argument("--method", required=True, choices=["wanda", "sparsegpt", "gptq", "awq"])
ap.add_argument("--sparsity", type=float, default=0.23,
help="prune fraction (0.23 == keep 0.77 == model_ratio 0.8 retention)")
ap.add_argument("--bits", type=int, default=4)
ap.add_argument("--prune_n", type=int, default=2,
help="N:M structured sparsity for wanda/sparsegpt (default 2:4, "
"hardware-accelerated). Set 0 for unstructured at --sparsity.")
ap.add_argument("--prune_m", type=int, default=4)
ap.add_argument("--groupsize", type=int, default=128)
ap.add_argument("--layer_type", choices=["all", "attn", "mlp"], default="all")
ap.add_argument("--model_path", default=C.DEFAULT_MODEL_PATH)
ap.add_argument("--save_path", required=True)
ap.add_argument("--nsamples", type=int, default=0)
ap.add_argument("--xtx_budget_gb", type=float, default=0.0,
help="max GB of XtX kept on GPU per collection pass. 0 = AUTO: "
"free GPU memory after the model is resident minus headroom, "
"so the same command runs on a 24GB or a 48GB card (only the "
"number of passes changes -- binning never touches math).")
ap.add_argument("--xtx_headroom_gb", type=float, default=6.0)
ap.add_argument("--xtx_cpu", action="store_true",
help="legacy per-sample CPU accumulation (slow; repro only)")
args = ap.parse_args()
device = "cuda" if torch.cuda.is_available() else "cpu"
os.makedirs(args.save_path, exist_ok=True)
t0 = time.time()
blob = torch.load(args.calib, map_location="cpu")
input_ids = blob["input_ids"]
if args.nsamples and args.nsamples < input_ids.shape[0]:
input_ids = input_ids[: args.nsamples]
arm = blob.get("arm", "unknown")
print(f"[baselines] method={args.method} calib={os.path.basename(args.calib)} "
f"arm={arm} N={input_ids.shape[0]} sparsity={args.sparsity} bits={args.bits}", flush=True)
model, tok = C.load_model(args.model_path, device=device)
targets = list(C.iter_target_linears(model, args.layer_type))
print(f"[baselines] target linears ({args.layer_type}) = {len(targets)}", flush=True)
if args.xtx_cpu:
cov = collect_xtx_cpu(model, input_ids, dict(targets), device, mask_id=C.MASK_ID)
else:
budget_gb = args.xtx_budget_gb
if budget_gb <= 0:
if device == "cuda":
free_b, total_b = torch.cuda.mem_get_info()
budget_gb = max(2.0, free_b / 1024 ** 3 - args.xtx_headroom_gb)
print(f"[baselines] AUTO xtx budget: free={free_b/1024**3:.1f}GB "
f"total={total_b/1024**3:.1f}GB headroom={args.xtx_headroom_gb}GB "
f"-> budget={budget_gb:.1f}GB", flush=True)
else:
budget_gb = 8.0
bins = plan_bins(targets, budget_gb * 1024 ** 3)
print(f"[baselines] {len(targets)} target linears -> {len(bins)} XtX pass(es) "
f"(budget {budget_gb:.1f}GB/pass)", flush=True)
cov = {}
for bi, b in enumerate(bins):
gb = sum(m.in_features ** 2 * 4 for m in b.values()) / 1024 ** 3
print(f"[baselines] XtX pass {bi+1}/{len(bins)}: {len(b)} layers, {gb:.1f}GB", flush=True)
cov.update(collect_xtx_gpu(model, input_ids, b, device,
log_prefix=f"p{bi+1} ", mask_id=C.MASK_ID))
print(f"[baselines] XtX collected for {len(cov)} layers ({time.time()-t0:.0f}s)", flush=True)
n_done = 0
for name, mod in targets:
if name not in cov:
continue
W = mod.weight.data.cpu().float().to(device)
H = cov[name].to(device)
if args.method == "wanda":
Wq = wanda_prune(W, H, args.sparsity, args.prune_n, args.prune_m)
elif args.method == "sparsegpt":
Wq = sparsegpt_prune(W, H, args.sparsity, prune_n=args.prune_n, prune_m=args.prune_m)
elif args.method == "gptq":
Wq = gptq_quantize(W, H, args.bits, args.groupsize)
elif args.method == "awq":
act = torch.sqrt(torch.diag(H).clamp(min=0)) # per-channel RMS ~ mean|x| proxy
Wq = awq_quantize(W, act, args.bits, args.groupsize, H=H)
torch.save(Wq.to(torch.bfloat16).cpu(),
os.path.join(args.save_path, f"{name.replace('.', '_')}_weight.pt"))
del W, H, Wq
torch.cuda.empty_cache()
n_done += 1
if n_done % 30 == 0:
print(f" [{n_done}/{len(targets)}] {name}", flush=True)
_structured = (args.method in ("wanda", "sparsegpt") and args.prune_n > 0)
summary = {"method": args.method, "arm": arm, "sparsity": args.sparsity,
"bits": args.bits, "groupsize": args.groupsize,
"structured": f"{args.prune_n}:{args.prune_m}" if _structured else "unstructured",
"real_sparsity": (args.prune_n / args.prune_m) if _structured else args.sparsity,
"layer_type": args.layer_type, "n_layers": n_done,
"calib_file": os.path.basename(args.calib), "git_hash": C.git_hash(),
"wall_clock_sec": time.time() - t0, "save_path": args.save_path}
C.dump_json(summary, os.path.join(args.save_path, "compression_summary.json"))
print(f"[baselines] done: {n_done} layers -> {args.save_path} ({(time.time()-t0)/60:.1f}min)", flush=True)
if __name__ == "__main__":
main()