Download scripts/quant_sweep.py from poolside-laguna-hackathon/kv-landlords: direct link, hf CLI and curl.
- Browser
- Download file 4.56 kB
-
https://huggingface.co/poolside-laguna-hackathon/kv-landlords/resolve/main/scripts/quant_sweep.py
- Command line
-
hf download hf://poolside-laguna-hackathon/kv-landlords/scripts/quant_sweep.py
-
curl -L -o quant_sweep.py https://huggingface.co/poolside-laguna-hackathon/kv-landlords/resolve/main/scripts/quant_sweep.py
4.56 kB
| """INT4-vs-NVFP4 KV-cache quantization sweep on Laguna-XS.2. | |
| For each layer's real K and V activations, measures reconstruction RMSE across | |
| the grid {nvfp4, int4} × {headdim-block, per-channel-block} × {absmax, mse}. | |
| The baseline cell (nvfp4 / headdim / absmax) is what vLLM's NVFP4 KV kernel ships | |
| today. Every cell has identical memory (4-bit data + one scale per 16 elements), | |
| so this isolates reconstruction quality. K and V are reported separately to | |
| expose any KIVI-style asymmetry (per-channel K, per-token V). | |
| Usage: | |
| python -m scripts.quant_sweep [--max-new 200] [--n-alphas 32] | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import sys | |
| import torch | |
| from transformers import AutoModelForCausalLM, AutoTokenizer, DynamicCache | |
| sys.path.insert(0, "/home/alex/poolside-hackathon-kv-quant") | |
| from kv_quant import BLOCK, SWEEP_CELLS, SWEEP_BASELINE, sweep_tensor | |
| MODEL = "poolside/Laguna-XS.2" | |
| PROMPT = ( | |
| "Solve step by step. A train leaves city A at 60 km/h. Two hours later a second " | |
| "train leaves the same station on the same track at 90 km/h. How many hours after " | |
| "the second train departs will it catch up to the first train? Show your reasoning." | |
| ) | |
| def _cell_label(cell) -> str: | |
| return f"{cell[0]:<6} {cell[1]:<8} {cell[2]:<6}" | |
| def _print_table(title: str, abs_rmse: dict, ratio: dict) -> None: | |
| base = SWEEP_BASELINE | |
| print(f"\n{title}") | |
| print(f" {'format':<6} {'layout':<8} {'calib':<6} {'avg RMSE':>10} {'vs vLLM':>10}") | |
| print(f" {'-'*6} {'-'*8} {'-'*6} {'-'*10} {'-'*10}") | |
| for c in [base] + [c for c in SWEEP_CELLS if c != base]: | |
| tag = "baseline" if c == base else f"{(1.0 - ratio[c]) * 100.0:+.1f}%" | |
| print(f" {_cell_label(c)} {abs_rmse[c]:>10.5f} {tag:>10}") | |
| best = min(SWEEP_CELLS, key=lambda c: ratio[c]) | |
| print(f" best: {best[0]}/{best[1]}/{best[2]} " | |
| f"({(1.0 - ratio[best]) * 100.0:+.1f}% vs baseline)") | |
| def run_sweep(per_layer_kv, n_alphas, device): | |
| """per_layer_kv: list of (K, V) [n_kv, seq, head_dim]. Returns means + ratios.""" | |
| base = SWEEP_BASELINE | |
| k_abs = {c: [] for c in SWEEP_CELLS} | |
| k_rat = {c: [] for c in SWEEP_CELLS} | |
| v_abs = {c: [] for c in SWEEP_CELLS} | |
| v_rat = {c: [] for c in SWEEP_CELLS} | |
| n_used = 0 | |
| for K, V in per_layer_kv: | |
| if K.shape[1] < BLOCK: | |
| continue | |
| n_used += 1 | |
| ks = sweep_tensor(K.to(device), n_alphas) | |
| vs = sweep_tensor(V.to(device), n_alphas) | |
| for c in SWEEP_CELLS: | |
| k_abs[c].append(ks[c]); k_rat[c].append(ks[c] / max(ks[base], 1e-12)) | |
| v_abs[c].append(vs[c]); v_rat[c].append(vs[c] / max(vs[base], 1e-12)) | |
| mean = lambda d: {c: sum(x) / max(len(x), 1) for c, x in d.items()} | |
| return n_used, mean(k_abs), mean(k_rat), mean(v_abs), mean(v_rat) | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--max-new", type=int, default=200) | |
| ap.add_argument("--n-alphas", type=int, default=32) | |
| args = ap.parse_args() | |
| print(f"[load] {MODEL} ...", flush=True) | |
| tok = AutoTokenizer.from_pretrained(MODEL) | |
| model = AutoModelForCausalLM.from_pretrained( | |
| MODEL, torch_dtype=torch.bfloat16, device_map="auto") | |
| model.eval() | |
| device = next(model.parameters()).device | |
| cfg = model.config | |
| print(f"[load] layers={cfg.num_hidden_layers} kv_heads={cfg.num_key_value_heads} " | |
| f"head_dim={getattr(cfg, 'head_dim', None)} device={device}", flush=True) | |
| msgs = [{"role": "user", "content": PROMPT}] | |
| input_ids = tok.apply_chat_template( | |
| msgs, add_generation_prompt=True, return_tensors="pt", return_dict=False, | |
| ).to(device) | |
| cache = DynamicCache() | |
| with torch.no_grad(): | |
| model.generate(input_ids, max_new_tokens=args.max_new, | |
| past_key_values=cache, use_cache=True, do_sample=False) | |
| per_layer = [(layer.keys[0].float().cpu(), layer.values[0].float().cpu()) | |
| for layer in cache.layers] | |
| seq = per_layer[0][0].shape[1] if per_layer else 0 | |
| print(f"[seq] {seq} tokens cached across {len(per_layer)} layers", flush=True) | |
| n_used, k_abs, k_rat, v_abs, v_rat = run_sweep(per_layer, args.n_alphas, device) | |
| print(f"\n{'='*56}") | |
| print(f"INT4 vs NVFP4 KV sweep — {n_used} layers, seq cropped to /{BLOCK}") | |
| print("All cells: identical memory (4-bit + 1 scale per 16 elems, ~3.56x vs BF16).") | |
| _print_table("KEY cache (avg RMSE over layers):", k_abs, k_rat) | |
| _print_table("VALUE cache (avg RMSE over layers):", v_abs, v_rat) | |
| if __name__ == "__main__": | |
| main() | |