| |
| """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 |
|
|
| 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) |
| wscale = reader.get(f"{prefix}.weight_scale").to(device) |
| wscale2 = reader.get(f"{prefix}.weight_scale_2").to(device) |
|
|
| lo = (packed & 0x0F).long() |
| hi = (packed >> 4).long() |
| vals = torch.empty( |
| packed.shape[0], packed.shape[1] * 2, dtype=torch.float32, device=device |
| ) |
| |
| 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) |
|
|
| |
| 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) |
| ) |
|
|
| |
| 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] |
|
|
| |
| 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() |
|
|