File size: 7,538 Bytes
6e668dc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | """Accuracy analysis for INT4 KV cache quantization on Laguna-XS.2.
Two measurements:
1. Per-layer reconstruction RMSE (all 40 layers, absmax vs MSE-optimal).
2. Token agreement: generate with BF16 cache vs INT4-simulated cache
(quantize+dequantize each layer's KV in-place at every decode step).
Usage:
python -m scripts.quant_accuracy [--max-new 200]
"""
from __future__ import annotations
import argparse
import sys
import time
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, DynamicCache
sys.path.insert(0, "/home/alex/poolside-hackathon-kv-quant")
from kv_quant import (
BLOCK, PAGE, QMAX,
_absmax_scale, _mse_optimal_scale,
quantize_block, dequantize_block,
quantize_page, QuantizedKVCache, measure_page_error,
)
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 layer_rmse(k: torch.Tensor, use_mse: bool) -> float:
"""RMSE of INT4 round-trip on one layer's key cache [n_kv, seq, head_dim]."""
kf = k.float().reshape(*k.shape[:-1], k.shape[-1] // BLOCK, BLOCK)
scale_fn = _mse_optimal_scale if use_mse else _absmax_scale
s = scale_fn(kf) if not use_mse else scale_fn(kf)
q = quantize_block(kf, s)
khat = dequantize_block(q, s)
return ((kf - khat) ** 2).mean().sqrt().item()
def simulate_int4_generation(model, input_ids, tok, max_new: int) -> list[int]:
"""Generate greedily; after each step, quantize+dequantize every layer's KV."""
cache = DynamicCache()
device = input_ids.device
with torch.no_grad():
# Prefill
out = model(input_ids=input_ids, past_key_values=cache, use_cache=True,
cache_position=torch.arange(input_ids.shape[1], device=device),
position_ids=torch.arange(input_ids.shape[1], device=device).unsqueeze(0))
_quantize_cache_inplace(cache)
tokens = [out.logits[0, -1].argmax().item()]
abs_pos = input_ids.shape[1]
for _ in range(max_new - 1):
cp = torch.tensor([abs_pos], device=device)
out = model(input_ids=torch.tensor([[tokens[-1]]], device=device),
past_key_values=cache, use_cache=True,
cache_position=cp,
position_ids=cp.unsqueeze(0))
_quantize_cache_inplace(cache)
t = out.logits[0, -1].argmax().item()
tokens.append(t)
abs_pos += 1
eos = getattr(model.config, "eos_token_id", None)
eos_set = set(eos) if isinstance(eos, (list, tuple)) else ({eos} if eos else set())
if t in eos_set:
break
return tokens
def _quantize_cache_inplace(cache: DynamicCache) -> None:
"""Quantize+dequantize every layer's keys and values in the cache (INT4 simulation)."""
for layer in cache.layers:
layer.keys = _round_trip(layer.keys)
layer.values = _round_trip(layer.values)
def _round_trip(x: torch.Tensor) -> torch.Tensor:
"""Quantize x [B, n_heads, seq, head_dim] to INT4 and dequantize back."""
B, H, S, D = x.shape
xf = x.float().reshape(B, H, S, D // BLOCK, BLOCK)
s = _mse_optimal_scale(xf)
q = quantize_block(xf, s)
return dequantize_block(q, s).reshape(B, H, S, D).to(x.dtype)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--max-new", type=int, default=200)
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
msgs = [{"role": "user", "content": PROMPT}]
input_ids = tok.apply_chat_template(
msgs, add_generation_prompt=True, return_tensors="pt", return_dict=False,
).to(device)
print(f"[prompt] {input_ids.shape[1]} tokens\n")
# ββ 1. BF16 baseline generation ββββββββββββββββββββββββββββββββββββββββββ
print("[run] BF16 baseline ...", flush=True)
cache_bf16 = DynamicCache()
t0 = time.time()
with torch.no_grad():
out_bf16 = model.generate(
input_ids, max_new_tokens=args.max_new,
past_key_values=cache_bf16, use_cache=True, do_sample=False,
)
bf16_t = time.time() - t0
bf16_tokens = out_bf16[0, input_ids.shape[1]:].tolist()
# ββ 2. Per-layer reconstruction stats ββββββββββββββββββββββββββββββββββββ
print("\n[stats] Per-layer key-cache RMSE (absmax vs MSE-optimal)")
print(f" {'layer':>5} {'absmax RMSE':>12} {'opt RMSE':>10} {'reduction':>10}")
print(f" {'β'*5} {'β'*12} {'β'*10} {'β'*10}")
all_abs, all_opt = [], []
for i, layer in enumerate(cache_bf16.layers):
k = layer.keys[0].float().cpu()
kf = k.reshape(*k.shape[:-1], k.shape[-1] // BLOCK, BLOCK)
s_abs = _absmax_scale(kf)
s_opt = _mse_optimal_scale(kf)
rmse_abs = ((kf - dequantize_block(quantize_block(kf, s_abs), s_abs)) ** 2).mean().sqrt().item()
rmse_opt = ((kf - dequantize_block(quantize_block(kf, s_opt), s_opt)) ** 2).mean().sqrt().item()
all_abs.append(rmse_abs)
all_opt.append(rmse_opt)
pct = 100.0 * (rmse_abs - rmse_opt) / max(rmse_abs, 1e-12)
print(f" {i:>5} {rmse_abs:>12.6f} {rmse_opt:>10.6f} {pct:>9.1f}%")
avg_abs = sum(all_abs) / len(all_abs)
avg_opt = sum(all_opt) / len(all_opt)
avg_red = 100.0 * (avg_abs - avg_opt) / max(avg_abs, 1e-12)
print(f" {'avg':>5} {avg_abs:>12.6f} {avg_opt:>10.6f} {avg_red:>9.1f}%")
# ββ 3. INT4-simulated generation βββββββββββββββββββββββββββββββββββββββββ
print(f"\n[run] INT4-simulated generation (quant+dequant each step) ...", flush=True)
t0 = time.time()
int4_tokens = simulate_int4_generation(model, input_ids, tok, args.max_new)
int4_t = time.time() - t0
# ββ 4. Token agreement ββββββββββββββββββββββββββββββββββββββββββββββββββββ
n = min(len(bf16_tokens), len(int4_tokens))
agree = sum(a == b for a, b in zip(bf16_tokens[:n], int4_tokens[:n]))
prefix = 0
for a, b in zip(bf16_tokens, int4_tokens):
if a != b:
break
prefix += 1
print("\n========== BF16 OUTPUT ==========")
print(tok.decode(bf16_tokens, skip_special_tokens=True)[:800])
print("\n========== INT4-SIMULATED OUTPUT ==========")
print(tok.decode(int4_tokens, skip_special_tokens=True)[:800])
print("\n========== ACCURACY SUMMARY ==========")
print(f" tokens compared: {n}")
print(f" token agreement: {agree}/{n} ({100*agree/max(n,1):.1f}%)")
print(f" identical prefix: {prefix} tokens")
print(f" avg key RMSE (absmax): {avg_abs:.6f}")
print(f" avg key RMSE (optimal): {avg_opt:.6f} ({avg_red:.1f}% reduction)")
print(f" BF16 time: {bf16_t:.1f}s INT4-sim time: {int4_t:.1f}s")
if __name__ == "__main__":
main()
|