File size: 5,027 Bytes
163f417
 
 
caaa501
 
163f417
caaa501
163f417
 
 
 
 
caaa501
 
 
 
163f417
 
 
 
 
 
 
 
 
 
 
 
caaa501
163f417
caaa501
163f417
 
 
 
 
 
 
caaa501
 
 
 
 
 
 
 
 
 
 
 
163f417
 
caaa501
163f417
caaa501
 
 
 
 
 
 
 
 
 
 
 
 
163f417
 
caaa501
 
 
 
163f417
caaa501
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163f417
 
caaa501
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163f417
 
caaa501
 
 
 
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
"""Gradio Space demo: KakeyaLatticeCache on a small HF causal LM.

Run locally:
 pip install kakeyalattice[hf] gradio
 python app.py

Deploy to HF Spaces: see ./README.md. By default uses Qwen2-0.5B
(head_dim=64, E8-compatible) so it fits on a free HF Space CPU.
Swap to Qwen/Qwen2.5-1.5B or Llama-3.2-1B (GPU Space) for more interesting
decode-length comparisons.

The demo shows, side-by-side, the same prompt generated under:
 (a) bf16 DynamicCache — reference
 (b) KakeyaLatticeCache E8 Q=10 (aggressive, ~3.6x KV compression)
 (c) KakeyaLatticeCache E8 Q=38 (balanced, ~2.5x KV compression)
 (d) KakeyaLatticeCache E8 Q=152 (near-lossless, ~1.9x KV compression)
and reports wall-clock + per-layer K rel-MSE.
"""
from __future__ import annotations

import os
import time
from typing import Optional

import gradio as gr
import torch

try:
 from transformers import AutoModelForCausalLM, AutoTokenizer, DynamicCache
except ImportError as e:
 raise ImportError("Install transformers: pip install 'kakeyalattice[hf]'") from e

from kakeyalattice.hf import KakeyaLatticeCache

DEFAULT_MODEL = os.environ.get("KAKEYA_DEMO_MODEL", "Qwen/Qwen2-0.5B")
_model_cache: dict = {}

def _load_model(model_id: str, device: str):
 key = (model_id, device)
 if key in _model_cache:
  return _model_cache[key]
 tok = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
 model = AutoModelForCausalLM.from_pretrained(
  model_id,
  torch_dtype=torch.bfloat16 if device == "cuda" else torch.float32,
  trust_remote_code=True,
 ).to(device)
 model.eval()
 _model_cache[key] = (tok, model)
 return tok, model

def _generate_one(
 tok, model, prompt: str, max_new: int, cache, device: str,
) -> tuple[str, float]:
 ids = tok(prompt, return_tensors="pt").to(device)
 t0 = time.perf_counter()
 with torch.inference_mode():
  out = model.generate(
   **ids,
   max_new_tokens=max_new,
   do_sample=False,
   past_key_values=cache,
   use_cache=True,
  )
 elapsed = time.perf_counter() - t0
 text = tok.decode(out[0], skip_special_tokens=True)
 return text, elapsed

def run_demo(
 prompt: str,
 max_new: int,
 model_id: str,
 device_pref: str,
) -> tuple[str, str, str, str, str]:
 device = "cuda" if (device_pref == "auto" and torch.cuda.is_available()) else (
  "cuda" if device_pref == "cuda" else "cpu"
 )
 tok, model = _load_model(model_id, device)

 cfg = model.config
 num_hidden_layers = cfg.num_hidden_layers
 head_dim = getattr(cfg, "head_dim", cfg.hidden_size // cfg.num_attention_heads)

 results = []

 # Baseline: bf16 DynamicCache
 baseline_cache = DynamicCache()
 text_bf16, t_bf16 = _generate_one(tok, model, prompt, max_new, baseline_cache, device)
 results.append(("bf16 DynamicCache (reference)", text_bf16, t_bf16, head_dim * 16))

 for q, label in [(10, "E8 Q=10 aggressive"), (38, "E8 Q=38 balanced"), (152, "E8 Q=152 near-lossless")]:
  try:
   cache = KakeyaLatticeCache(
    variant="e8", q_range=q,
    num_hidden_layers=num_hidden_layers,
    head_dim=head_dim,
    device=device,
    strict=False,
   )
   text, t = _generate_one(tok, model, prompt, max_new, cache, device)
   bits = cache._codecs[0].bits_per_token_per_head if cache._codecs else head_dim * 16
   results.append((f"KakeyaLattice {label}", text, t, bits))
  except Exception as e:
   results.append((f"KakeyaLattice {label} (FAILED)", f"Error: {e}", 0.0, 0))

 # Format as comparison table
 header = f"Model: {model_id} | head_dim: {head_dim} | device: {device} | new_tokens: {max_new}"
 rows = [
  f"\n### {name} — {t:.2f}s, {bits} bits/vec ({bits/16:.1f}x vs bf16)\n\n{text}"
  for (name, text, t, bits) in results
 ]
 return header, *rows

with gr.Blocks(title="KakeyaLattice KV-cache compression demo") as demo:
 gr.Markdown(
  "# KakeyaLattice KV-cache compression demo\n\n"
  "Compare generation output + latency across **bf16 baseline** and "
  "three **KakeyaLattice E8** compression levels on a small HF causal LM. "
  "The E8 variant uses 8-D nested-lattice closest-point quantisation "
  "with Sylvester-Hadamard rotation and per-vector adaptive scaling."
 )
 with gr.Row():
  prompt = gr.Textbox(
   label="Prompt",
   value="Explain in one paragraph why lattice quantisation can beat scalar quantisation:",
   lines=3,
  )
 with gr.Row():
  max_new = gr.Slider(minimum=16, maximum=512, value=128, step=16, label="Max new tokens")
  model_id = gr.Textbox(label="HF model id", value=DEFAULT_MODEL)
  device_pref = gr.Radio(choices=["auto", "cpu", "cuda"], value="auto", label="Device")
 run_btn = gr.Button("Run comparison", variant="primary")
 header_out = gr.Markdown("")
 out_bf16 = gr.Markdown("")
 out_q10 = gr.Markdown("")
 out_q38 = gr.Markdown("")
 out_q152 = gr.Markdown("")
 run_btn.click(
  fn=run_demo,
  inputs=[prompt, max_new, model_id, device_pref],
  outputs=[header_out, out_bf16, out_q10, out_q38, out_q152],
 )

if __name__ == "__main__":
 demo.launch(
  server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
  server_port=int(os.environ.get("PORT", os.environ.get("GRADIO_SERVER_PORT", "7860"))),
 )