Download code/tools/aqlm_quantize.py from FlyCockpit/GLM-5.2-NVFP4-AQLM-Abliterated-Vision: direct link, hf CLI and curl.
- Browser
- Download file 11.5 kB
-
https://huggingface.co/FlyCockpit/GLM-5.2-NVFP4-AQLM-Abliterated-Vision/resolve/main/code/tools/aqlm_quantize.py
- Command line
-
hf download hf://FlyCockpit/GLM-5.2-NVFP4-AQLM-Abliterated-Vision/code/tools/aqlm_quantize.py
-
curl -L -o aqlm_quantize.py https://huggingface.co/FlyCockpit/GLM-5.2-NVFP4-AQLM-Abliterated-Vision/resolve/main/code/tools/aqlm_quantize.py
11.5 kB
| #!/usr/bin/env python3 | |
| """AQLM quantization of GLM-5.2 routed experts from the NVFP4 checkpoint. | |
| For each AQLM-assigned layer (see hybrid_plan.json): | |
| 1. Load the layer's 256 experts' NVFP4 tensors from the downloaded repo, | |
| dequantize to bf16 on GPU (u8-packed fp4 * fp8 block scale * fp32 | |
| global scale). | |
| 2. Per projection (w13 = gate rows then up rows; w2 = down): | |
| a. per-output-channel scale s[e,o] (mean abs of the row); | |
| b. fit one 65536 x 8 codebook per book with GPU k-means on a row | |
| sample (codebook shared across all 256 experts); | |
| c. encode all groups-of-8 by chunked nearest-centroid search | |
| (books=2 encodes the residual against a second codebook); | |
| d. least-squares refit of the per-channel scales. | |
| 3. Save codes/codebooks/scales + reconstruction stats per layer. | |
| Everything is chunked: peak GPU memory is the bf16 expert tensor plus a | |
| few GB of scratch, so 8 workers (one per GPU) run comfortably. | |
| Usage: aqlm_quantize.py [--layers 11,12] [--gpus 0,1,...] [--out DIR] | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import time | |
| from concurrent.futures import ProcessPoolExecutor | |
| ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| NVFP4_DIR = os.environ.get("GLM52_NVFP4_DIR", "/tmp/glm52-dl/nvfp4-full") | |
| OUT_DIR = "/data/glm52-aqlm-parts" | |
| ENTRIES = 65536 | |
| GDIM = 8 | |
| SAMPLE_VECS = 4_000_000 | |
| KMEANS_ITERS = 12 | |
| CHUNK_VECS = 32_768 # scores are [CHUNK_VECS, 65536] fp16 (~4.3GB) | |
| FP4_LUT = [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, | |
| -0.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0] | |
| def load_plan(): | |
| plan = json.load(open(os.path.join(ROOT, "hybrid_plan.json"))) | |
| def books(tier): | |
| t = plan["aqlm"][tier] | |
| return {"w13": t["w13_books"], "w2": t["w2_books"]} | |
| layers = {} | |
| for li in plan["aqlm_mixed_layers"]: | |
| layers[li] = books("mixed") | |
| for li in plan["aqlm_cold_layers"]: | |
| layers[li] = books("cold") | |
| return layers | |
| class ShardReader: | |
| """Random access to tensors across the downloaded safetensors shards.""" | |
| def __init__(self, repo_dir: str): | |
| idx = json.load( | |
| open(os.path.join(repo_dir, "model.safetensors.index.json")) | |
| ) | |
| self.weight_map = idx["weight_map"] | |
| self.repo_dir = repo_dir | |
| self._open = {} | |
| def get(self, name: str): | |
| from safetensors import safe_open | |
| shard = self.weight_map[name] | |
| if shard not in self._open: | |
| self._open[shard] = safe_open( | |
| os.path.join(self.repo_dir, shard), framework="pt" | |
| ) | |
| return self._open[shard].get_tensor(name) | |
| def dequant_nvfp4(reader, prefix: str, device, lut) -> "torch.Tensor": | |
| """Dequantize one NVFP4 linear weight to bf16 [out, in].""" | |
| import torch | |
| packed = reader.get(f"{prefix}.weight").to(device) # u8 [out, in/2] | |
| wscale = reader.get(f"{prefix}.weight_scale").to(device) # fp8 [out, in/16] | |
| wscale2 = reader.get(f"{prefix}.weight_scale_2").to(device) # fp32 scalar | |
| lo = (packed & 0x0F).long() | |
| hi = (packed >> 4).long() | |
| vals = torch.empty( | |
| packed.shape[0], packed.shape[1] * 2, dtype=torch.float32, device=device | |
| ) | |
| # NVFP4 packs the first element of each pair in the low nibble. | |
| vals[:, 0::2] = lut[lo] | |
| vals[:, 1::2] = lut[hi] | |
| del lo, hi | |
| scale = wscale.to(torch.float32).repeat_interleave(16, dim=1) | |
| return (vals * scale * wscale2.to(torch.float32)).to(torch.bfloat16) | |
| def _argmin_codes(v16, cent16, cn16): | |
| """v16 [n,8] fp16, cent16 [ENTRIES,8] fp16 -> nearest ids int32. | |
| argmax of 2*v.c - |c|^2 (|v|^2 constant per vector).""" | |
| import torch | |
| d = v16 @ cent16.t() | |
| d.mul_(2).sub_(cn16) | |
| return d.argmax(-1).to(torch.int32) | |
| def encode_chunked(vecs16, cent16): | |
| import torch | |
| cn16 = (cent16.float() * cent16.float()).sum(-1).half() | |
| out = torch.empty( | |
| vecs16.shape[0], dtype=torch.int32, device=vecs16.device | |
| ) | |
| for s in range(0, vecs16.shape[0], CHUNK_VECS): | |
| out[s : s + CHUNK_VECS] = _argmin_codes( | |
| vecs16[s : s + CHUNK_VECS], cent16, cn16 | |
| ) | |
| return out | |
| def kmeans_fit(vecs16, iters: int, generator): | |
| """Lloyd's k-means on [N,8] fp16; returns [ENTRIES,8] fp16 centroids.""" | |
| import torch | |
| n = vecs16.shape[0] | |
| device = vecs16.device | |
| perm = torch.randperm(n, generator=generator, device=device)[:ENTRIES] | |
| cent = vecs16[perm].float() | |
| for _ in range(iters): | |
| cent16 = cent.half() | |
| cn16 = (cent * cent).sum(-1).half() | |
| sums = torch.zeros(ENTRIES, GDIM, device=device) | |
| cnts = torch.zeros(ENTRIES, device=device) | |
| ones = None | |
| for s in range(0, n, CHUNK_VECS): | |
| v = vecs16[s : s + CHUNK_VECS] | |
| a = _argmin_codes(v, cent16, cn16).long() | |
| sums.index_add_(0, a, v.float()) | |
| if ones is None or ones.shape[0] != a.shape[0]: | |
| ones = torch.ones(a.shape[0], device=device) | |
| cnts.index_add_(0, a, ones[: a.shape[0]]) | |
| mask = cnts > 0 | |
| cent[mask] = sums[mask] / cnts[mask].unsqueeze(-1) | |
| dead = int((~mask).sum()) | |
| if dead: | |
| ridx = torch.randperm(n, generator=generator, device=device)[:dead] | |
| cent[~mask] = vecs16[ridx].float() | |
| return cent.half() | |
| def quantize_matrix(w, books: int, generator, log): | |
| """w: [E, out, in] bf16 (GPU) -> dict with codes int16 [E,books,out,in/8], | |
| codebooks fp16 [books,ENTRIES,8], scales fp16 [E,out], rel mse.""" | |
| import torch | |
| e, out, k = w.shape | |
| device = w.device | |
| rows = e * out | |
| k8 = k // GDIM | |
| wv = w.reshape(rows, k) | |
| # per-row scales, chunked | |
| scales = torch.empty(rows, dtype=torch.float32, device=device) | |
| ROWCH = max(1, (CHUNK_VECS * 64) // k) | |
| for s in range(0, rows, ROWCH): | |
| scales[s : s + ROWCH] = ( | |
| wv[s : s + ROWCH].float().abs().mean(-1).clamp_min(1e-8) | |
| ) | |
| # --- fit codebooks on a row sample --- | |
| n_sample_rows = min(rows, max(1, SAMPLE_VECS // k8)) | |
| ridx = torch.randperm(rows, generator=generator, device=device)[ | |
| :n_sample_rows | |
| ] | |
| samp = ( | |
| (wv[ridx].float() / scales[ridx].unsqueeze(-1)) | |
| .reshape(-1, GDIM) | |
| .half() | |
| ) | |
| log(f" kmeans book0 on {samp.shape[0]} vecs") | |
| cbs = [kmeans_fit(samp, KMEANS_ITERS, generator)] | |
| if books == 2: | |
| c0 = encode_chunked(samp, cbs[0]) | |
| resid = (samp.float() - cbs[0].float()[c0.long()]).half() | |
| log(f" kmeans book1 on residuals") | |
| cbs.append(kmeans_fit(resid, KMEANS_ITERS, generator)) | |
| del c0, resid | |
| del samp | |
| torch.cuda.empty_cache() | |
| cb_f = [c.float() for c in cbs] | |
| cn16 = [(cf * cf).sum(-1).half() for cf in cb_f] | |
| # --- full encode, chunked by rows; accumulate scale-refit stats --- | |
| codes = torch.empty( | |
| (rows, books, k8), dtype=torch.int16, device=device | |
| ) | |
| num = torch.zeros(rows, dtype=torch.float32, device=device) | |
| den = torch.zeros(rows, dtype=torch.float32, device=device) | |
| sse = 0.0 | |
| stot = 0.0 | |
| ROWCH = max(1, CHUNK_VECS // k8) | |
| for s in range(0, rows, ROWCH): | |
| wc = wv[s : s + ROWCH].float() / scales[s : s + ROWCH].unsqueeze(-1) | |
| v = wc.reshape(-1, GDIM).half() | |
| approx = torch.zeros(v.shape[0], GDIM, device=device) | |
| r = v.float() | |
| for b in range(books): | |
| cb = _argmin_codes(r.half(), cbs[b], cn16[b]).long() | |
| dec = cb_f[b][cb] | |
| approx += dec | |
| r -= dec | |
| codes[s : s + ROWCH, b] = ( | |
| torch.where(cb >= 32768, cb - 65536, cb) | |
| .to(torch.int16) | |
| .reshape(-1, k8) | |
| ) | |
| nrow = wc.shape[0] | |
| a2 = approx.reshape(nrow, k) | |
| w2 = wc | |
| num[s : s + ROWCH] = (w2 * a2).sum(-1) | |
| den[s : s + ROWCH] = (a2 * a2).sum(-1).clamp_min(1e-8) | |
| sse += float((r * r).sum()) | |
| stot += float((w2 * w2).sum()) | |
| del wc, v, approx, r, a2 | |
| rel = sse / max(stot, 1e-9) | |
| scales = scales * (num / den).clamp(0.5, 2.0) | |
| log(f" rel_mse={rel:.4f}") | |
| return { | |
| "codes": codes.reshape(e, out, books, k8) | |
| .permute(0, 2, 1, 3) | |
| .contiguous() | |
| .cpu(), | |
| "codebooks": torch.stack(cbs).cpu(), | |
| "scales": scales.reshape(e, out).half().cpu(), | |
| "rel_mse": rel, | |
| } | |
| def process_layer(layer_idx: int, books: dict, gpu: int): | |
| import torch | |
| t0 = time.time() | |
| device = f"cuda:{gpu}" | |
| torch.cuda.set_device(gpu) | |
| gen = torch.Generator(device=device) | |
| gen.manual_seed(1234 + layer_idx) | |
| lut = torch.tensor(FP4_LUT, dtype=torch.float32, device=device) | |
| def log(msg): | |
| print(f"[L{layer_idx} gpu{gpu}] {msg}", flush=True) | |
| out_path = os.path.join(OUT_DIR, f"layer_{layer_idx}.pt") | |
| if os.path.exists(out_path): | |
| log("already done, skip") | |
| return layer_idx, None | |
| reader = ShardReader(NVFP4_DIR) | |
| n_exp, inter, hidden = 256, 2048, 6144 | |
| log("dequantizing experts") | |
| w13 = torch.empty( | |
| n_exp, 2 * inter, hidden, dtype=torch.bfloat16, device=device | |
| ) | |
| w2 = torch.empty(n_exp, hidden, inter, dtype=torch.bfloat16, device=device) | |
| for ei in range(n_exp): | |
| p = f"model.layers.{layer_idx}.mlp.experts.{ei}" | |
| w13[ei, :inter] = dequant_nvfp4(reader, f"{p}.gate_proj", device, lut) | |
| w13[ei, inter:] = dequant_nvfp4(reader, f"{p}.up_proj", device, lut) | |
| w2[ei] = dequant_nvfp4(reader, f"{p}.down_proj", device, lut) | |
| log(f"w13 {tuple(w13.shape)} books={books['w13']}") | |
| w13_out = quantize_matrix(w13, books["w13"], gen, log) | |
| del w13 | |
| torch.cuda.empty_cache() | |
| log(f"w2 {tuple(w2.shape)} books={books['w2']}") | |
| w2_out = quantize_matrix(w2, books["w2"], gen, log) | |
| del w2 | |
| torch.cuda.empty_cache() | |
| torch.save( | |
| { | |
| "layer": layer_idx, | |
| "books": books, | |
| "w13_codes": w13_out["codes"], | |
| "w13_codebooks": w13_out["codebooks"], | |
| "w13_scales": w13_out["scales"], | |
| "w13_rel_mse": w13_out["rel_mse"], | |
| "w2_codes": w2_out["codes"], | |
| "w2_codebooks": w2_out["codebooks"], | |
| "w2_scales": w2_out["scales"], | |
| "w2_rel_mse": w2_out["rel_mse"], | |
| }, | |
| out_path, | |
| ) | |
| log(f"saved ({time.time()-t0:.0f}s)") | |
| return layer_idx, (w13_out["rel_mse"], w2_out["rel_mse"]) | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--layers", default=None) | |
| ap.add_argument("--gpus", default="0,1,2,3,4,5,6,7") | |
| ap.add_argument("--out", default=OUT_DIR) | |
| args = ap.parse_args() | |
| globals()["OUT_DIR"] = args.out | |
| os.makedirs(args.out, exist_ok=True) | |
| plan = load_plan() | |
| layers = sorted(plan) | |
| if args.layers: | |
| want = {int(x) for x in args.layers.split(",")} | |
| layers = [li for li in layers if li in want] | |
| gpus = [int(g) for g in args.gpus.split(",")] | |
| print(f"{len(layers)} layers over {len(gpus)} GPUs", flush=True) | |
| jobs = [(li, plan[li], gpus[i % len(gpus)]) for i, li in enumerate(layers)] | |
| with ProcessPoolExecutor(max_workers=len(gpus)) as ex: | |
| futs = [ex.submit(process_layer, *j) for j in jobs] | |
| for f in futs: | |
| li, rel = f.result() | |
| if rel: | |
| print( | |
| f"== layer {li}: rel_mse w13={rel[0]:.4f} w2={rel[1]:.4f}", | |
| flush=True, | |
| ) | |
| if __name__ == "__main__": | |
| main() | |