adirik commited on
Commit
5a2d2ad
·
1 Parent(s): 1d36ea8

AttnVQ submission

Browse files
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ .DS_Store
2
+ .DS_Store
README.md CHANGED
@@ -1,3 +1,91 @@
1
  ---
2
  license: apache-2.0
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: apache-2.0
3
  ---
4
+
5
+ # AttnVQ — Attention-Aware KV Cache Quantization
6
+
7
+ Training-free **product vector quantization** of the KV cache for long-context LLMs. AttnVQ fits small per-subspace codebooks with LBG, but scores distortion by **attention-output error** (and key cosine / inner-product bias), not cache MSE. Calibration is light: **10–15 agent traces, ~15 s on GPU** — enough to capture the **model's** K/V geometry (data-aware, not corpus-dependent).
8
+
9
+ Primary target: **Laguna-XS.2** (model-agnostic). Only the **10 full-attention layers** are compressed; 30 sliding-window layers stay fp16.
10
+
11
+ ## What this repo offers
12
+
13
+ | Component | Description |
14
+ |---|---|
15
+ | **`generate.py`** | Minimal inference: `VQQuantizedCache` → `model.generate()` (uint8 indices, real memory savings) |
16
+ | **`vqkv/`** | Quantizers (ProductVQ, RoPESplit, scalar/KIVI baselines), attention-aware metrics, compressed cache |
17
+ | **`benchmark.py`** | Fit codebooks + **cheap metrics** (key cosine, attn-output error, ip-bias) on real cache dumps |
18
+ | **`turbo_benchmark.py`** | Faithful **TurboQuant** baseline (Haar rotation + Lloyd-Max + QJL) |
19
+ | **`longbench_eval.py`** | LongBench v1 proxy metrics + optional end-to-end task scoring |
20
+ | **`artifacts/`** | Pre-fit codebooks and LongBench results |
21
+
22
+ **Variants:** `productvq-*` (AttnVQ), `ropesplit-1b` (RoPE-half split for Laguna), scalar/KIVI/sign/ternary baselines, TurboQuant MSE/Prod.
23
+
24
+ ## Headline results (Laguna-XS.2)
25
+
26
+ **Memory @ 131K context** (full-attention layers only):
27
+
28
+ | Config | KV cache |
29
+ |---|---|
30
+ | fp16 | 5.4 GB |
31
+ | AttnVQ 2-bit (`productvq-32x256-2b`) | 0.73 GB (7.4×) |
32
+ | AttnVQ 1-bit (`productvq-16x256-1b`) | 0.40 GB (14×) |
33
+
34
+ **LongBench v1** (mean F1 over qasper, 2wikimqa, hotpotqa, repobench-p; single 15-trace codebook):
35
+
36
+ - **2-bit:** ~96% of fp16 — TurboQuant ~83%, INT2 ~75%
37
+ - **1-bit:** AttnVQ and RoPESplit beat every iso-budget baseline on every task
38
+ - **0.5-bit:** only VQ reaches this regime at all
39
+
40
+ Full numbers: `artifacts/longbench_results.json`, `artifacts/longbench_cheap_metrics.json`.
41
+
42
+ **Note:** Wall-clock speedup requires a fused dequant kernel.
43
+
44
+
45
+ ## Quick start
46
+ Tested on CUDA 12.4 / NVIDIA A100.
47
+
48
+ ```bash
49
+ pip install "git+https://github.com/huggingface/transformers.git" \
50
+ accelerate datasets torch==2.9.1 torchvision tqdm
51
+ python generate.py
52
+ ```
53
+
54
+ Use fitted codebooks for memory efficient long context generation:
55
+ ```py
56
+ import torch
57
+ from transformers import AutoModelForCausalLM, AutoTokenizer
58
+ from vqkv.compressed_cache import VQQuantizedCache
59
+
60
+
61
+ # load model
62
+ tok = AutoTokenizer.from_pretrained("poolside/Laguna-XS.2", trust_remote_code=True, fix_mistral_regex=True)
63
+ model = AutoModelForCausalLM.from_pretrained("poolside/Laguna-XS.2", torch_dtype=torch.bfloat16, device_map="cuda", trust_remote_code=True).eval()
64
+
65
+ # load codebooks or fit and use your own
66
+ CODEBOOKS_PATH = "artifacts/codebooks.pt"
67
+ codebooks = torch.load(CODEBOOKS_PATH, map_location="cuda", weights_only=False)
68
+
69
+ # build cache
70
+ quantizers, layers = codebooks["fitted"]["productvq-32x256-2b"], blob["meta"]["full_layers"]
71
+ cache = VQQuantizedCache(quantizers, layers) # persists uint8 codebook indices
72
+
73
+ # generate
74
+ ids = tok("Hello", return_tensors="pt").to(model.device)
75
+ out = model.generate(**ids, max_new_tokens=32, past_key_values=cache, use_cache=True)
76
+ print(tok.decode(out[0, ids["input_ids"].shape[1]:], skip_special_tokens=True))
77
+
78
+ # print memory footprint
79
+ print(cache.memory_footprint())
80
+ ```
81
+
82
+ ## Reproduce
83
+
84
+ Precomputed results are under `artifacts/`. To re-fit and evaluate:
85
+
86
+ ```bash
87
+ python benchmark.py --stage fit
88
+ python turbo_benchmark.py --stage fit
89
+ python longbench_eval.py --stage cheap --n_eval 50 # cheap metrics
90
+ python longbench_eval.py --stage generate --n_eval 50 # slow: full generation & task metrics
91
+ ```
artifacts/codebooks.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:30f0ac721169cfc655f1d6ca5ac07bc817ec434b6149437fb247e5052be11028
3
+ size 17489175
artifacts/longbench_cheap_metrics.json ADDED
@@ -0,0 +1,1192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "config": "scalar-int4",
4
+ "bits_per_elt": 4.25,
5
+ "n_traces": 50,
6
+ "key_cos": 0.99257,
7
+ "val_cos": 0.99503,
8
+ "key_mse": 0.02338,
9
+ "val_mse": 0.0001,
10
+ "attn_cos": 0.99966,
11
+ "attn_output_error": 0.02973,
12
+ "ip_rel": 1.34455,
13
+ "ip_bias": 0.0001,
14
+ "task": "qasper"
15
+ },
16
+ {
17
+ "config": "scalar-int2",
18
+ "bits_per_elt": 2.25,
19
+ "n_traces": 50,
20
+ "key_cos": 0.86766,
21
+ "val_cos": 0.89301,
22
+ "key_mse": 0.46759,
23
+ "val_mse": 0.00257,
24
+ "attn_cos": 0.99046,
25
+ "attn_output_error": 0.15708,
26
+ "ip_rel": 4.9622,
27
+ "ip_bias": 0.00096,
28
+ "task": "qasper"
29
+ },
30
+ {
31
+ "config": "kivi-int2",
32
+ "bits_per_elt": 2.25,
33
+ "n_traces": 50,
34
+ "key_cos": 0.86152,
35
+ "val_cos": 0.89301,
36
+ "key_mse": 0.54308,
37
+ "val_mse": 0.00257,
38
+ "attn_cos": 0.99043,
39
+ "attn_output_error": 0.15729,
40
+ "ip_rel": 5.98639,
41
+ "ip_bias": -5e-05,
42
+ "task": "qasper"
43
+ },
44
+ {
45
+ "config": "turboquant~-int2",
46
+ "bits_per_elt": 2.125,
47
+ "n_traces": 50,
48
+ "key_cos": 0.64016,
49
+ "val_cos": 0.64168,
50
+ "key_mse": 2.17849,
51
+ "val_mse": 0.01394,
52
+ "attn_cos": 0.36733,
53
+ "attn_output_error": 2.47328,
54
+ "ip_rel": 9.66254,
55
+ "ip_bias": -0.00053,
56
+ "task": "qasper"
57
+ },
58
+ {
59
+ "config": "productvq-16x256-1b",
60
+ "bits_per_elt": 1.0,
61
+ "n_traces": 50,
62
+ "key_cos": 0.8606,
63
+ "val_cos": 0.82426,
64
+ "key_mse": 0.43329,
65
+ "val_mse": 0.00342,
66
+ "attn_cos": 0.97447,
67
+ "attn_output_error": 0.33079,
68
+ "ip_rel": 4.05013,
69
+ "ip_bias": 0.00015,
70
+ "task": "qasper"
71
+ },
72
+ {
73
+ "config": "productvq-8x256-0.5b",
74
+ "bits_per_elt": 0.5,
75
+ "n_traces": 50,
76
+ "key_cos": 0.73386,
77
+ "val_cos": 0.67074,
78
+ "key_mse": 0.76039,
79
+ "val_mse": 0.00583,
80
+ "attn_cos": 0.91293,
81
+ "attn_output_error": 0.50347,
82
+ "ip_rel": 4.9189,
83
+ "ip_bias": 0.00143,
84
+ "task": "qasper"
85
+ },
86
+ {
87
+ "config": "ropesplit-1b",
88
+ "bits_per_elt": 1.0,
89
+ "n_traces": 50,
90
+ "key_cos": 0.84851,
91
+ "val_cos": 0.82369,
92
+ "key_mse": 0.46147,
93
+ "val_mse": 0.00342,
94
+ "attn_cos": 0.97455,
95
+ "attn_output_error": 0.33236,
96
+ "ip_rel": 3.94593,
97
+ "ip_bias": 0.00051,
98
+ "task": "qasper"
99
+ },
100
+ {
101
+ "config": "sign-1bit",
102
+ "bits_per_elt": 1.125,
103
+ "n_traces": 50,
104
+ "key_cos": 0.82715,
105
+ "val_cos": 0.79949,
106
+ "key_mse": 0.51141,
107
+ "val_mse": 0.00367,
108
+ "attn_cos": 0.97079,
109
+ "attn_output_error": 0.40264,
110
+ "ip_rel": 3.99374,
111
+ "ip_bias": 0.00027,
112
+ "task": "qasper"
113
+ },
114
+ {
115
+ "config": "ternary-bitnet",
116
+ "bits_per_elt": 1.71,
117
+ "n_traces": 50,
118
+ "key_cos": 0.91306,
119
+ "val_cos": 0.90047,
120
+ "key_mse": 0.26966,
121
+ "val_mse": 0.00192,
122
+ "attn_cos": 0.98765,
123
+ "attn_output_error": 0.2377,
124
+ "ip_rel": 3.23327,
125
+ "ip_bias": -6e-05,
126
+ "task": "qasper"
127
+ },
128
+ {
129
+ "config": "productvq-64x256-4b",
130
+ "bits_per_elt": 4.0,
131
+ "n_traces": 50,
132
+ "key_cos": 0.99599,
133
+ "val_cos": 0.99485,
134
+ "key_mse": 0.01346,
135
+ "val_mse": 0.00012,
136
+ "attn_cos": 0.99959,
137
+ "attn_output_error": 0.03627,
138
+ "ip_rel": 0.76086,
139
+ "ip_bias": -2e-05,
140
+ "task": "qasper"
141
+ },
142
+ {
143
+ "config": "productvq-32x256-2b",
144
+ "bits_per_elt": 2.0,
145
+ "n_traces": 50,
146
+ "key_cos": 0.95936,
147
+ "val_cos": 0.94781,
148
+ "key_mse": 0.13408,
149
+ "val_mse": 0.00108,
150
+ "attn_cos": 0.99537,
151
+ "attn_output_error": 0.13766,
152
+ "ip_rel": 2.72405,
153
+ "ip_bias": 0.00031,
154
+ "task": "qasper"
155
+ },
156
+ {
157
+ "config": "turbo-mse-4b",
158
+ "bits_per_elt": 4.125,
159
+ "n_traces": 50,
160
+ "key_cos": 0.99504,
161
+ "val_cos": 0.99504,
162
+ "key_mse": 0.01612,
163
+ "val_mse": 0.0001,
164
+ "attn_cos": 0.9996,
165
+ "attn_output_error": 0.03475,
166
+ "ip_rel": 0.9187,
167
+ "ip_bias": -3e-05,
168
+ "task": "qasper"
169
+ },
170
+ {
171
+ "config": "turbo-prod-4b (K:+qjl)",
172
+ "bits_per_elt": 5.125,
173
+ "n_traces": 50,
174
+ "key_cos": 0.99504,
175
+ "val_cos": 0.99504,
176
+ "key_mse": 0.01613,
177
+ "val_mse": 0.0001,
178
+ "attn_cos": 0.9996,
179
+ "attn_output_error": 0.03478,
180
+ "ip_rel": 1.04771,
181
+ "ip_bias": 4e-05,
182
+ "task": "qasper"
183
+ },
184
+ {
185
+ "config": "turbo-mse-2b",
186
+ "bits_per_elt": 2.125,
187
+ "n_traces": 50,
188
+ "key_cos": 0.94071,
189
+ "val_cos": 0.9407,
190
+ "key_mse": 0.18395,
191
+ "val_mse": 0.00118,
192
+ "attn_cos": 0.99373,
193
+ "attn_output_error": 0.16035,
194
+ "ip_rel": 3.38128,
195
+ "ip_bias": 0.00046,
196
+ "task": "qasper"
197
+ },
198
+ {
199
+ "config": "turbo-prod-2b (K:+qjl)",
200
+ "bits_per_elt": 3.125,
201
+ "n_traces": 50,
202
+ "key_cos": 0.94071,
203
+ "val_cos": 0.94071,
204
+ "key_mse": 0.18395,
205
+ "val_mse": 0.00118,
206
+ "attn_cos": 0.99373,
207
+ "attn_output_error": 0.1604,
208
+ "ip_rel": 3.49197,
209
+ "ip_bias": -0.00021,
210
+ "task": "qasper"
211
+ },
212
+ {
213
+ "config": "turbo-mse-1b",
214
+ "bits_per_elt": 1.125,
215
+ "n_traces": 50,
216
+ "key_cos": 0.7997,
217
+ "val_cos": 0.79979,
218
+ "key_mse": 0.57262,
219
+ "val_mse": 0.00367,
220
+ "attn_cos": 0.97083,
221
+ "attn_output_error": 0.40155,
222
+ "ip_rel": 5.04924,
223
+ "ip_bias": -0.00028,
224
+ "task": "qasper"
225
+ },
226
+ {
227
+ "config": "turbo-prod-1b (K:+qjl)",
228
+ "bits_per_elt": 2.125,
229
+ "n_traces": 50,
230
+ "key_cos": 0.7997,
231
+ "val_cos": 0.79979,
232
+ "key_mse": 0.57262,
233
+ "val_mse": 0.00367,
234
+ "attn_cos": 0.97084,
235
+ "attn_output_error": 0.40162,
236
+ "ip_rel": 7.50776,
237
+ "ip_bias": -6e-05,
238
+ "task": "qasper"
239
+ },
240
+ {
241
+ "config": "scalar-int4",
242
+ "bits_per_elt": 4.25,
243
+ "n_traces": 47,
244
+ "key_cos": 0.99254,
245
+ "val_cos": 0.99503,
246
+ "key_mse": 0.02381,
247
+ "val_mse": 0.0001,
248
+ "attn_cos": 0.99968,
249
+ "attn_output_error": 0.02836,
250
+ "ip_rel": 0.95936,
251
+ "ip_bias": 0.00013,
252
+ "task": "2wikimqa"
253
+ },
254
+ {
255
+ "config": "scalar-int2",
256
+ "bits_per_elt": 2.25,
257
+ "n_traces": 47,
258
+ "key_cos": 0.86637,
259
+ "val_cos": 0.89305,
260
+ "key_mse": 0.48028,
261
+ "val_mse": 0.00262,
262
+ "attn_cos": 0.99088,
263
+ "attn_output_error": 0.15136,
264
+ "ip_rel": 4.48127,
265
+ "ip_bias": 0.00021,
266
+ "task": "2wikimqa"
267
+ },
268
+ {
269
+ "config": "kivi-int2",
270
+ "bits_per_elt": 2.25,
271
+ "n_traces": 47,
272
+ "key_cos": 0.85589,
273
+ "val_cos": 0.89305,
274
+ "key_mse": 0.57652,
275
+ "val_mse": 0.00262,
276
+ "attn_cos": 0.99085,
277
+ "attn_output_error": 0.1516,
278
+ "ip_rel": 5.84995,
279
+ "ip_bias": -0.0007,
280
+ "task": "2wikimqa"
281
+ },
282
+ {
283
+ "config": "turboquant~-int2",
284
+ "bits_per_elt": 2.125,
285
+ "n_traces": 47,
286
+ "key_cos": 0.64105,
287
+ "val_cos": 0.64241,
288
+ "key_mse": 2.21716,
289
+ "val_mse": 0.0142,
290
+ "attn_cos": 0.39052,
291
+ "attn_output_error": 2.37009,
292
+ "ip_rel": 13.81316,
293
+ "ip_bias": 0.0022,
294
+ "task": "2wikimqa"
295
+ },
296
+ {
297
+ "config": "productvq-16x256-1b",
298
+ "bits_per_elt": 1.0,
299
+ "n_traces": 47,
300
+ "key_cos": 0.86048,
301
+ "val_cos": 0.82425,
302
+ "key_mse": 0.44288,
303
+ "val_mse": 0.00349,
304
+ "attn_cos": 0.97475,
305
+ "attn_output_error": 0.34054,
306
+ "ip_rel": 4.03823,
307
+ "ip_bias": 0.00044,
308
+ "task": "2wikimqa"
309
+ },
310
+ {
311
+ "config": "productvq-8x256-0.5b",
312
+ "bits_per_elt": 0.5,
313
+ "n_traces": 47,
314
+ "key_cos": 0.7317,
315
+ "val_cos": 0.66903,
316
+ "key_mse": 0.78213,
317
+ "val_mse": 0.00598,
318
+ "attn_cos": 0.91045,
319
+ "attn_output_error": 0.52276,
320
+ "ip_rel": 4.68996,
321
+ "ip_bias": 0.00175,
322
+ "task": "2wikimqa"
323
+ },
324
+ {
325
+ "config": "ropesplit-1b",
326
+ "bits_per_elt": 1.0,
327
+ "n_traces": 47,
328
+ "key_cos": 0.84739,
329
+ "val_cos": 0.8237,
330
+ "key_mse": 0.4739,
331
+ "val_mse": 0.0035,
332
+ "attn_cos": 0.97487,
333
+ "attn_output_error": 0.34152,
334
+ "ip_rel": 4.43052,
335
+ "ip_bias": 0.00079,
336
+ "task": "2wikimqa"
337
+ },
338
+ {
339
+ "config": "sign-1bit",
340
+ "bits_per_elt": 1.125,
341
+ "n_traces": 47,
342
+ "key_cos": 0.82765,
343
+ "val_cos": 0.79952,
344
+ "key_mse": 0.51975,
345
+ "val_mse": 0.00374,
346
+ "attn_cos": 0.97069,
347
+ "attn_output_error": 0.40168,
348
+ "ip_rel": 4.59382,
349
+ "ip_bias": -0.00051,
350
+ "task": "2wikimqa"
351
+ },
352
+ {
353
+ "config": "ternary-bitnet",
354
+ "bits_per_elt": 1.71,
355
+ "n_traces": 47,
356
+ "key_cos": 0.91339,
357
+ "val_cos": 0.90049,
358
+ "key_mse": 0.27376,
359
+ "val_mse": 0.00196,
360
+ "attn_cos": 0.98759,
361
+ "attn_output_error": 0.23654,
362
+ "ip_rel": 3.65978,
363
+ "ip_bias": 0.00062,
364
+ "task": "2wikimqa"
365
+ },
366
+ {
367
+ "config": "productvq-64x256-4b",
368
+ "bits_per_elt": 4.0,
369
+ "n_traces": 47,
370
+ "key_cos": 0.99596,
371
+ "val_cos": 0.99487,
372
+ "key_mse": 0.01392,
373
+ "val_mse": 0.00012,
374
+ "attn_cos": 0.9996,
375
+ "attn_output_error": 0.03607,
376
+ "ip_rel": 0.86073,
377
+ "ip_bias": -5e-05,
378
+ "task": "2wikimqa"
379
+ },
380
+ {
381
+ "config": "productvq-32x256-2b",
382
+ "bits_per_elt": 2.0,
383
+ "n_traces": 47,
384
+ "key_cos": 0.95941,
385
+ "val_cos": 0.94804,
386
+ "key_mse": 0.13688,
387
+ "val_mse": 0.0011,
388
+ "attn_cos": 0.99554,
389
+ "attn_output_error": 0.13911,
390
+ "ip_rel": 2.10964,
391
+ "ip_bias": 5e-05,
392
+ "task": "2wikimqa"
393
+ },
394
+ {
395
+ "config": "turbo-mse-4b",
396
+ "bits_per_elt": 4.125,
397
+ "n_traces": 47,
398
+ "key_cos": 0.99505,
399
+ "val_cos": 0.99504,
400
+ "key_mse": 0.01637,
401
+ "val_mse": 0.00011,
402
+ "attn_cos": 0.99961,
403
+ "attn_output_error": 0.03411,
404
+ "ip_rel": 0.86796,
405
+ "ip_bias": 8e-05,
406
+ "task": "2wikimqa"
407
+ },
408
+ {
409
+ "config": "turbo-prod-4b (K:+qjl)",
410
+ "bits_per_elt": 5.125,
411
+ "n_traces": 47,
412
+ "key_cos": 0.99505,
413
+ "val_cos": 0.99504,
414
+ "key_mse": 0.01637,
415
+ "val_mse": 0.00011,
416
+ "attn_cos": 0.99961,
417
+ "attn_output_error": 0.03415,
418
+ "ip_rel": 1.15955,
419
+ "ip_bias": 1e-05,
420
+ "task": "2wikimqa"
421
+ },
422
+ {
423
+ "config": "turbo-mse-2b",
424
+ "bits_per_elt": 2.125,
425
+ "n_traces": 47,
426
+ "key_cos": 0.94077,
427
+ "val_cos": 0.94072,
428
+ "key_mse": 0.18695,
429
+ "val_mse": 0.0012,
430
+ "attn_cos": 0.99375,
431
+ "attn_output_error": 0.15968,
432
+ "ip_rel": 2.78259,
433
+ "ip_bias": 0.00031,
434
+ "task": "2wikimqa"
435
+ },
436
+ {
437
+ "config": "turbo-prod-2b (K:+qjl)",
438
+ "bits_per_elt": 3.125,
439
+ "n_traces": 47,
440
+ "key_cos": 0.94077,
441
+ "val_cos": 0.94072,
442
+ "key_mse": 0.18696,
443
+ "val_mse": 0.0012,
444
+ "attn_cos": 0.99375,
445
+ "attn_output_error": 0.15975,
446
+ "ip_rel": 3.76935,
447
+ "ip_bias": -0.00026,
448
+ "task": "2wikimqa"
449
+ },
450
+ {
451
+ "config": "turbo-mse-1b",
452
+ "bits_per_elt": 1.125,
453
+ "n_traces": 47,
454
+ "key_cos": 0.79981,
455
+ "val_cos": 0.79987,
456
+ "key_mse": 0.58236,
457
+ "val_mse": 0.00374,
458
+ "attn_cos": 0.97061,
459
+ "attn_output_error": 0.40143,
460
+ "ip_rel": 4.56288,
461
+ "ip_bias": -0.00021,
462
+ "task": "2wikimqa"
463
+ },
464
+ {
465
+ "config": "turbo-prod-1b (K:+qjl)",
466
+ "bits_per_elt": 2.125,
467
+ "n_traces": 47,
468
+ "key_cos": 0.79981,
469
+ "val_cos": 0.79986,
470
+ "key_mse": 0.58237,
471
+ "val_mse": 0.00374,
472
+ "attn_cos": 0.97061,
473
+ "attn_output_error": 0.40152,
474
+ "ip_rel": 6.74448,
475
+ "ip_bias": -0.00105,
476
+ "task": "2wikimqa"
477
+ },
478
+ {
479
+ "config": "scalar-int4",
480
+ "bits_per_elt": 4.25,
481
+ "n_traces": 50,
482
+ "key_cos": 0.99257,
483
+ "val_cos": 0.99502,
484
+ "key_mse": 0.02373,
485
+ "val_mse": 0.0001,
486
+ "attn_cos": 0.99969,
487
+ "attn_output_error": 0.02754,
488
+ "ip_rel": 1.21463,
489
+ "ip_bias": -0.0001,
490
+ "task": "hotpotqa"
491
+ },
492
+ {
493
+ "config": "scalar-int2",
494
+ "bits_per_elt": 2.25,
495
+ "n_traces": 50,
496
+ "key_cos": 0.86642,
497
+ "val_cos": 0.893,
498
+ "key_mse": 0.48208,
499
+ "val_mse": 0.0026,
500
+ "attn_cos": 0.99106,
501
+ "attn_output_error": 0.14817,
502
+ "ip_rel": 4.89558,
503
+ "ip_bias": 0.00069,
504
+ "task": "hotpotqa"
505
+ },
506
+ {
507
+ "config": "kivi-int2",
508
+ "bits_per_elt": 2.25,
509
+ "n_traces": 50,
510
+ "key_cos": 0.84639,
511
+ "val_cos": 0.893,
512
+ "key_mse": 0.61765,
513
+ "val_mse": 0.0026,
514
+ "attn_cos": 0.99102,
515
+ "attn_output_error": 0.14845,
516
+ "ip_rel": 6.11069,
517
+ "ip_bias": 0.0004,
518
+ "task": "hotpotqa"
519
+ },
520
+ {
521
+ "config": "turboquant~-int2",
522
+ "bits_per_elt": 2.125,
523
+ "n_traces": 50,
524
+ "key_cos": 0.64118,
525
+ "val_cos": 0.64228,
526
+ "key_mse": 2.22279,
527
+ "val_mse": 0.01408,
528
+ "attn_cos": 0.3972,
529
+ "attn_output_error": 2.2878,
530
+ "ip_rel": 10.22977,
531
+ "ip_bias": 0.00486,
532
+ "task": "hotpotqa"
533
+ },
534
+ {
535
+ "config": "productvq-16x256-1b",
536
+ "bits_per_elt": 1.0,
537
+ "n_traces": 50,
538
+ "key_cos": 0.85939,
539
+ "val_cos": 0.82403,
540
+ "key_mse": 0.44685,
541
+ "val_mse": 0.00347,
542
+ "attn_cos": 0.97507,
543
+ "attn_output_error": 0.34394,
544
+ "ip_rel": 3.87267,
545
+ "ip_bias": -0.00045,
546
+ "task": "hotpotqa"
547
+ },
548
+ {
549
+ "config": "productvq-8x256-0.5b",
550
+ "bits_per_elt": 0.5,
551
+ "n_traces": 50,
552
+ "key_cos": 0.72974,
553
+ "val_cos": 0.66901,
554
+ "key_mse": 0.78885,
555
+ "val_mse": 0.00593,
556
+ "attn_cos": 0.9105,
557
+ "attn_output_error": 0.52521,
558
+ "ip_rel": 4.54048,
559
+ "ip_bias": 0.00067,
560
+ "task": "hotpotqa"
561
+ },
562
+ {
563
+ "config": "ropesplit-1b",
564
+ "bits_per_elt": 1.0,
565
+ "n_traces": 50,
566
+ "key_cos": 0.8452,
567
+ "val_cos": 0.82348,
568
+ "key_mse": 0.48031,
569
+ "val_mse": 0.00347,
570
+ "attn_cos": 0.97524,
571
+ "attn_output_error": 0.34501,
572
+ "ip_rel": 4.81682,
573
+ "ip_bias": -0.00074,
574
+ "task": "hotpotqa"
575
+ },
576
+ {
577
+ "config": "sign-1bit",
578
+ "bits_per_elt": 1.125,
579
+ "n_traces": 50,
580
+ "key_cos": 0.82812,
581
+ "val_cos": 0.79949,
582
+ "key_mse": 0.51988,
583
+ "val_mse": 0.00371,
584
+ "attn_cos": 0.97041,
585
+ "attn_output_error": 0.40237,
586
+ "ip_rel": 4.76672,
587
+ "ip_bias": -0.00047,
588
+ "task": "hotpotqa"
589
+ },
590
+ {
591
+ "config": "ternary-bitnet",
592
+ "bits_per_elt": 1.71,
593
+ "n_traces": 50,
594
+ "key_cos": 0.91369,
595
+ "val_cos": 0.90047,
596
+ "key_mse": 0.27374,
597
+ "val_mse": 0.00194,
598
+ "attn_cos": 0.98747,
599
+ "attn_output_error": 0.23722,
600
+ "ip_rel": 3.34637,
601
+ "ip_bias": 0.00039,
602
+ "task": "hotpotqa"
603
+ },
604
+ {
605
+ "config": "productvq-64x256-4b",
606
+ "bits_per_elt": 4.0,
607
+ "n_traces": 50,
608
+ "key_cos": 0.99591,
609
+ "val_cos": 0.99486,
610
+ "key_mse": 0.01412,
611
+ "val_mse": 0.00012,
612
+ "attn_cos": 0.9996,
613
+ "attn_output_error": 0.03707,
614
+ "ip_rel": 0.79562,
615
+ "ip_bias": -7e-05,
616
+ "task": "hotpotqa"
617
+ },
618
+ {
619
+ "config": "productvq-32x256-2b",
620
+ "bits_per_elt": 2.0,
621
+ "n_traces": 50,
622
+ "key_cos": 0.9591,
623
+ "val_cos": 0.94788,
624
+ "key_mse": 0.13817,
625
+ "val_mse": 0.00109,
626
+ "attn_cos": 0.99563,
627
+ "attn_output_error": 0.14155,
628
+ "ip_rel": 3.0345,
629
+ "ip_bias": 0.00029,
630
+ "task": "hotpotqa"
631
+ },
632
+ {
633
+ "config": "turbo-mse-4b",
634
+ "bits_per_elt": 4.125,
635
+ "n_traces": 50,
636
+ "key_cos": 0.99504,
637
+ "val_cos": 0.99504,
638
+ "key_mse": 0.01644,
639
+ "val_mse": 0.0001,
640
+ "attn_cos": 0.99962,
641
+ "attn_output_error": 0.0335,
642
+ "ip_rel": 0.95323,
643
+ "ip_bias": -1e-05,
644
+ "task": "hotpotqa"
645
+ },
646
+ {
647
+ "config": "turbo-prod-4b (K:+qjl)",
648
+ "bits_per_elt": 5.125,
649
+ "n_traces": 50,
650
+ "key_cos": 0.99504,
651
+ "val_cos": 0.99504,
652
+ "key_mse": 0.01644,
653
+ "val_mse": 0.0001,
654
+ "attn_cos": 0.99962,
655
+ "attn_output_error": 0.03354,
656
+ "ip_rel": 1.13378,
657
+ "ip_bias": 0.00023,
658
+ "task": "hotpotqa"
659
+ },
660
+ {
661
+ "config": "turbo-mse-2b",
662
+ "bits_per_elt": 2.125,
663
+ "n_traces": 50,
664
+ "key_cos": 0.94075,
665
+ "val_cos": 0.94072,
666
+ "key_mse": 0.18757,
667
+ "val_mse": 0.00119,
668
+ "attn_cos": 0.99379,
669
+ "attn_output_error": 0.15863,
670
+ "ip_rel": 3.15094,
671
+ "ip_bias": -0.00013,
672
+ "task": "hotpotqa"
673
+ },
674
+ {
675
+ "config": "turbo-prod-2b (K:+qjl)",
676
+ "bits_per_elt": 3.125,
677
+ "n_traces": 50,
678
+ "key_cos": 0.94075,
679
+ "val_cos": 0.94072,
680
+ "key_mse": 0.18757,
681
+ "val_mse": 0.00119,
682
+ "attn_cos": 0.99379,
683
+ "attn_output_error": 0.1587,
684
+ "ip_rel": 3.73498,
685
+ "ip_bias": 9e-05,
686
+ "task": "hotpotqa"
687
+ },
688
+ {
689
+ "config": "turbo-mse-1b",
690
+ "bits_per_elt": 1.125,
691
+ "n_traces": 50,
692
+ "key_cos": 0.79977,
693
+ "val_cos": 0.79987,
694
+ "key_mse": 0.58414,
695
+ "val_mse": 0.00371,
696
+ "attn_cos": 0.97038,
697
+ "attn_output_error": 0.40117,
698
+ "ip_rel": 3.84738,
699
+ "ip_bias": 7e-05,
700
+ "task": "hotpotqa"
701
+ },
702
+ {
703
+ "config": "turbo-prod-1b (K:+qjl)",
704
+ "bits_per_elt": 2.125,
705
+ "n_traces": 50,
706
+ "key_cos": 0.79976,
707
+ "val_cos": 0.79986,
708
+ "key_mse": 0.58415,
709
+ "val_mse": 0.00371,
710
+ "attn_cos": 0.97038,
711
+ "attn_output_error": 0.40125,
712
+ "ip_rel": 6.33608,
713
+ "ip_bias": 0.0003,
714
+ "task": "hotpotqa"
715
+ },
716
+ {
717
+ "config": "scalar-int4",
718
+ "bits_per_elt": 4.25,
719
+ "n_traces": 50,
720
+ "key_cos": 0.99253,
721
+ "val_cos": 0.99503,
722
+ "key_mse": 0.02406,
723
+ "val_mse": 0.00011,
724
+ "attn_cos": 0.99968,
725
+ "attn_output_error": 0.02813,
726
+ "ip_rel": 1.03821,
727
+ "ip_bias": -0.00024,
728
+ "task": "passage_retrieval_en"
729
+ },
730
+ {
731
+ "config": "scalar-int2",
732
+ "bits_per_elt": 2.25,
733
+ "n_traces": 50,
734
+ "key_cos": 0.86483,
735
+ "val_cos": 0.89305,
736
+ "key_mse": 0.493,
737
+ "val_mse": 0.00263,
738
+ "attn_cos": 0.99115,
739
+ "attn_output_error": 0.14928,
740
+ "ip_rel": 5.79172,
741
+ "ip_bias": 0.00156,
742
+ "task": "passage_retrieval_en"
743
+ },
744
+ {
745
+ "config": "kivi-int2",
746
+ "bits_per_elt": 2.25,
747
+ "n_traces": 50,
748
+ "key_cos": 0.84588,
749
+ "val_cos": 0.89305,
750
+ "key_mse": 0.62044,
751
+ "val_mse": 0.00263,
752
+ "attn_cos": 0.99111,
753
+ "attn_output_error": 0.14955,
754
+ "ip_rel": 4.84881,
755
+ "ip_bias": 0.00017,
756
+ "task": "passage_retrieval_en"
757
+ },
758
+ {
759
+ "config": "turboquant~-int2",
760
+ "bits_per_elt": 2.125,
761
+ "n_traces": 50,
762
+ "key_cos": 0.64121,
763
+ "val_cos": 0.64206,
764
+ "key_mse": 2.23468,
765
+ "val_mse": 0.01425,
766
+ "attn_cos": 0.386,
767
+ "attn_output_error": 2.34058,
768
+ "ip_rel": 11.80499,
769
+ "ip_bias": 0.00126,
770
+ "task": "passage_retrieval_en"
771
+ },
772
+ {
773
+ "config": "productvq-16x256-1b",
774
+ "bits_per_elt": 1.0,
775
+ "n_traces": 50,
776
+ "key_cos": 0.86038,
777
+ "val_cos": 0.82379,
778
+ "key_mse": 0.44569,
779
+ "val_mse": 0.00351,
780
+ "attn_cos": 0.97498,
781
+ "attn_output_error": 0.34639,
782
+ "ip_rel": 3.6548,
783
+ "ip_bias": 0.00048,
784
+ "task": "passage_retrieval_en"
785
+ },
786
+ {
787
+ "config": "productvq-8x256-0.5b",
788
+ "bits_per_elt": 0.5,
789
+ "n_traces": 50,
790
+ "key_cos": 0.73171,
791
+ "val_cos": 0.66755,
792
+ "key_mse": 0.78719,
793
+ "val_mse": 0.00602,
794
+ "attn_cos": 0.9103,
795
+ "attn_output_error": 0.52946,
796
+ "ip_rel": 4.94806,
797
+ "ip_bias": -0.00091,
798
+ "task": "passage_retrieval_en"
799
+ },
800
+ {
801
+ "config": "ropesplit-1b",
802
+ "bits_per_elt": 1.0,
803
+ "n_traces": 50,
804
+ "key_cos": 0.84593,
805
+ "val_cos": 0.82324,
806
+ "key_mse": 0.48009,
807
+ "val_mse": 0.00352,
808
+ "attn_cos": 0.97512,
809
+ "attn_output_error": 0.34746,
810
+ "ip_rel": 4.24382,
811
+ "ip_bias": 0.00107,
812
+ "task": "passage_retrieval_en"
813
+ },
814
+ {
815
+ "config": "sign-1bit",
816
+ "bits_per_elt": 1.125,
817
+ "n_traces": 50,
818
+ "key_cos": 0.82941,
819
+ "val_cos": 0.7995,
820
+ "key_mse": 0.51788,
821
+ "val_mse": 0.00375,
822
+ "attn_cos": 0.9717,
823
+ "attn_output_error": 0.40183,
824
+ "ip_rel": 4.47009,
825
+ "ip_bias": -0.00038,
826
+ "task": "passage_retrieval_en"
827
+ },
828
+ {
829
+ "config": "ternary-bitnet",
830
+ "bits_per_elt": 1.71,
831
+ "n_traces": 50,
832
+ "key_cos": 0.91408,
833
+ "val_cos": 0.90049,
834
+ "key_mse": 0.27345,
835
+ "val_mse": 0.00197,
836
+ "attn_cos": 0.98803,
837
+ "attn_output_error": 0.23646,
838
+ "ip_rel": 3.31466,
839
+ "ip_bias": 0.00039,
840
+ "task": "passage_retrieval_en"
841
+ },
842
+ {
843
+ "config": "productvq-64x256-4b",
844
+ "bits_per_elt": 4.0,
845
+ "n_traces": 50,
846
+ "key_cos": 0.99592,
847
+ "val_cos": 0.99482,
848
+ "key_mse": 0.01414,
849
+ "val_mse": 0.00012,
850
+ "attn_cos": 0.9996,
851
+ "attn_output_error": 0.03737,
852
+ "ip_rel": 0.97058,
853
+ "ip_bias": 0.00012,
854
+ "task": "passage_retrieval_en"
855
+ },
856
+ {
857
+ "config": "productvq-32x256-2b",
858
+ "bits_per_elt": 2.0,
859
+ "n_traces": 50,
860
+ "key_cos": 0.9593,
861
+ "val_cos": 0.94794,
862
+ "key_mse": 0.13801,
863
+ "val_mse": 0.0011,
864
+ "attn_cos": 0.99558,
865
+ "attn_output_error": 0.1428,
866
+ "ip_rel": 2.48128,
867
+ "ip_bias": -0.00033,
868
+ "task": "passage_retrieval_en"
869
+ },
870
+ {
871
+ "config": "turbo-mse-4b",
872
+ "bits_per_elt": 4.125,
873
+ "n_traces": 50,
874
+ "key_cos": 0.99504,
875
+ "val_cos": 0.99504,
876
+ "key_mse": 0.01652,
877
+ "val_mse": 0.00011,
878
+ "attn_cos": 0.99962,
879
+ "attn_output_error": 0.03385,
880
+ "ip_rel": 0.90062,
881
+ "ip_bias": -0.00015,
882
+ "task": "passage_retrieval_en"
883
+ },
884
+ {
885
+ "config": "turbo-prod-4b (K:+qjl)",
886
+ "bits_per_elt": 5.125,
887
+ "n_traces": 50,
888
+ "key_cos": 0.99504,
889
+ "val_cos": 0.99504,
890
+ "key_mse": 0.01652,
891
+ "val_mse": 0.00011,
892
+ "attn_cos": 0.99962,
893
+ "attn_output_error": 0.0339,
894
+ "ip_rel": 1.23727,
895
+ "ip_bias": -0.00021,
896
+ "task": "passage_retrieval_en"
897
+ },
898
+ {
899
+ "config": "turbo-mse-2b",
900
+ "bits_per_elt": 2.125,
901
+ "n_traces": 50,
902
+ "key_cos": 0.94075,
903
+ "val_cos": 0.94073,
904
+ "key_mse": 0.18858,
905
+ "val_mse": 0.00121,
906
+ "attn_cos": 0.99395,
907
+ "attn_output_error": 0.1586,
908
+ "ip_rel": 3.42166,
909
+ "ip_bias": 0.00037,
910
+ "task": "passage_retrieval_en"
911
+ },
912
+ {
913
+ "config": "turbo-prod-2b (K:+qjl)",
914
+ "bits_per_elt": 3.125,
915
+ "n_traces": 50,
916
+ "key_cos": 0.94075,
917
+ "val_cos": 0.94073,
918
+ "key_mse": 0.18858,
919
+ "val_mse": 0.00121,
920
+ "attn_cos": 0.99395,
921
+ "attn_output_error": 0.15867,
922
+ "ip_rel": 3.99742,
923
+ "ip_bias": 0.00031,
924
+ "task": "passage_retrieval_en"
925
+ },
926
+ {
927
+ "config": "turbo-mse-1b",
928
+ "bits_per_elt": 1.125,
929
+ "n_traces": 50,
930
+ "key_cos": 0.79977,
931
+ "val_cos": 0.79989,
932
+ "key_mse": 0.58729,
933
+ "val_mse": 0.00375,
934
+ "attn_cos": 0.97165,
935
+ "attn_output_error": 0.4003,
936
+ "ip_rel": 4.29184,
937
+ "ip_bias": 0.00013,
938
+ "task": "passage_retrieval_en"
939
+ },
940
+ {
941
+ "config": "turbo-prod-1b (K:+qjl)",
942
+ "bits_per_elt": 2.125,
943
+ "n_traces": 50,
944
+ "key_cos": 0.79976,
945
+ "val_cos": 0.79989,
946
+ "key_mse": 0.5873,
947
+ "val_mse": 0.00375,
948
+ "attn_cos": 0.97165,
949
+ "attn_output_error": 0.4004,
950
+ "ip_rel": 6.17042,
951
+ "ip_bias": -0.00022,
952
+ "task": "passage_retrieval_en"
953
+ },
954
+ {
955
+ "config": "scalar-int4",
956
+ "bits_per_elt": 4.25,
957
+ "n_traces": 50,
958
+ "key_cos": 0.99235,
959
+ "val_cos": 0.99503,
960
+ "key_mse": 0.02448,
961
+ "val_mse": 0.0001,
962
+ "attn_cos": 0.99966,
963
+ "attn_output_error": 0.02895,
964
+ "ip_rel": 1.16444,
965
+ "ip_bias": -8e-05,
966
+ "task": "repobench-p"
967
+ },
968
+ {
969
+ "config": "scalar-int2",
970
+ "bits_per_elt": 2.25,
971
+ "n_traces": 50,
972
+ "key_cos": 0.86482,
973
+ "val_cos": 0.89308,
974
+ "key_mse": 0.48425,
975
+ "val_mse": 0.00243,
976
+ "attn_cos": 0.99033,
977
+ "attn_output_error": 0.15413,
978
+ "ip_rel": 5.37172,
979
+ "ip_bias": -0.00046,
980
+ "task": "repobench-p"
981
+ },
982
+ {
983
+ "config": "kivi-int2",
984
+ "bits_per_elt": 2.25,
985
+ "n_traces": 50,
986
+ "key_cos": 0.85308,
987
+ "val_cos": 0.89308,
988
+ "key_mse": 0.59033,
989
+ "val_mse": 0.00243,
990
+ "attn_cos": 0.9903,
991
+ "attn_output_error": 0.1544,
992
+ "ip_rel": 5.35,
993
+ "ip_bias": -0.0002,
994
+ "task": "repobench-p"
995
+ },
996
+ {
997
+ "config": "turboquant~-int2",
998
+ "bits_per_elt": 2.125,
999
+ "n_traces": 50,
1000
+ "key_cos": 0.64046,
1001
+ "val_cos": 0.64283,
1002
+ "key_mse": 2.21611,
1003
+ "val_mse": 0.01318,
1004
+ "attn_cos": 0.38991,
1005
+ "attn_output_error": 2.35685,
1006
+ "ip_rel": 11.24984,
1007
+ "ip_bias": -0.00501,
1008
+ "task": "repobench-p"
1009
+ },
1010
+ {
1011
+ "config": "productvq-16x256-1b",
1012
+ "bits_per_elt": 1.0,
1013
+ "n_traces": 50,
1014
+ "key_cos": 0.86444,
1015
+ "val_cos": 0.8248,
1016
+ "key_mse": 0.43171,
1017
+ "val_mse": 0.0032,
1018
+ "attn_cos": 0.97492,
1019
+ "attn_output_error": 0.32172,
1020
+ "ip_rel": 4.49992,
1021
+ "ip_bias": -0.0003,
1022
+ "task": "repobench-p"
1023
+ },
1024
+ {
1025
+ "config": "productvq-8x256-0.5b",
1026
+ "bits_per_elt": 0.5,
1027
+ "n_traces": 50,
1028
+ "key_cos": 0.74333,
1029
+ "val_cos": 0.67596,
1030
+ "key_mse": 0.75351,
1031
+ "val_mse": 0.00546,
1032
+ "attn_cos": 0.91412,
1033
+ "attn_output_error": 0.49052,
1034
+ "ip_rel": 6.03475,
1035
+ "ip_bias": -0.00016,
1036
+ "task": "repobench-p"
1037
+ },
1038
+ {
1039
+ "config": "ropesplit-1b",
1040
+ "bits_per_elt": 1.0,
1041
+ "n_traces": 50,
1042
+ "key_cos": 0.8528,
1043
+ "val_cos": 0.82423,
1044
+ "key_mse": 0.45971,
1045
+ "val_mse": 0.0032,
1046
+ "attn_cos": 0.97494,
1047
+ "attn_output_error": 0.32322,
1048
+ "ip_rel": 4.06041,
1049
+ "ip_bias": -0.0002,
1050
+ "task": "repobench-p"
1051
+ },
1052
+ {
1053
+ "config": "sign-1bit",
1054
+ "bits_per_elt": 1.125,
1055
+ "n_traces": 50,
1056
+ "key_cos": 0.82897,
1057
+ "val_cos": 0.79969,
1058
+ "key_mse": 0.5174,
1059
+ "val_mse": 0.00347,
1060
+ "attn_cos": 0.96847,
1061
+ "attn_output_error": 0.40384,
1062
+ "ip_rel": 4.84825,
1063
+ "ip_bias": -0.00062,
1064
+ "task": "repobench-p"
1065
+ },
1066
+ {
1067
+ "config": "ternary-bitnet",
1068
+ "bits_per_elt": 1.71,
1069
+ "n_traces": 50,
1070
+ "key_cos": 0.91402,
1071
+ "val_cos": 0.90054,
1072
+ "key_mse": 0.27299,
1073
+ "val_mse": 0.00182,
1074
+ "attn_cos": 0.9867,
1075
+ "attn_output_error": 0.23861,
1076
+ "ip_rel": 2.86088,
1077
+ "ip_bias": -0.0002,
1078
+ "task": "repobench-p"
1079
+ },
1080
+ {
1081
+ "config": "productvq-64x256-4b",
1082
+ "bits_per_elt": 4.0,
1083
+ "n_traces": 50,
1084
+ "key_cos": 0.99606,
1085
+ "val_cos": 0.99508,
1086
+ "key_mse": 0.01358,
1087
+ "val_mse": 0.00011,
1088
+ "attn_cos": 0.99962,
1089
+ "attn_output_error": 0.0346,
1090
+ "ip_rel": 0.71153,
1091
+ "ip_bias": -6e-05,
1092
+ "task": "repobench-p"
1093
+ },
1094
+ {
1095
+ "config": "productvq-32x256-2b",
1096
+ "bits_per_elt": 2.0,
1097
+ "n_traces": 50,
1098
+ "key_cos": 0.9603,
1099
+ "val_cos": 0.94747,
1100
+ "key_mse": 0.13409,
1101
+ "val_mse": 0.001,
1102
+ "attn_cos": 0.99551,
1103
+ "attn_output_error": 0.13328,
1104
+ "ip_rel": 2.39037,
1105
+ "ip_bias": -5e-05,
1106
+ "task": "repobench-p"
1107
+ },
1108
+ {
1109
+ "config": "turbo-mse-4b",
1110
+ "bits_per_elt": 4.125,
1111
+ "n_traces": 50,
1112
+ "key_cos": 0.99504,
1113
+ "val_cos": 0.99503,
1114
+ "key_mse": 0.01641,
1115
+ "val_mse": 0.0001,
1116
+ "attn_cos": 0.99958,
1117
+ "attn_output_error": 0.03482,
1118
+ "ip_rel": 0.9464,
1119
+ "ip_bias": 6e-05,
1120
+ "task": "repobench-p"
1121
+ },
1122
+ {
1123
+ "config": "turbo-prod-4b (K:+qjl)",
1124
+ "bits_per_elt": 5.125,
1125
+ "n_traces": 50,
1126
+ "key_cos": 0.99504,
1127
+ "val_cos": 0.99503,
1128
+ "key_mse": 0.01641,
1129
+ "val_mse": 0.0001,
1130
+ "attn_cos": 0.99958,
1131
+ "attn_output_error": 0.03486,
1132
+ "ip_rel": 1.26089,
1133
+ "ip_bias": 7e-05,
1134
+ "task": "repobench-p"
1135
+ },
1136
+ {
1137
+ "config": "turbo-mse-2b",
1138
+ "bits_per_elt": 2.125,
1139
+ "n_traces": 50,
1140
+ "key_cos": 0.94071,
1141
+ "val_cos": 0.94064,
1142
+ "key_mse": 0.18716,
1143
+ "val_mse": 0.00112,
1144
+ "attn_cos": 0.99335,
1145
+ "attn_output_error": 0.16086,
1146
+ "ip_rel": 3.0084,
1147
+ "ip_bias": 0.00063,
1148
+ "task": "repobench-p"
1149
+ },
1150
+ {
1151
+ "config": "turbo-prod-2b (K:+qjl)",
1152
+ "bits_per_elt": 3.125,
1153
+ "n_traces": 50,
1154
+ "key_cos": 0.94071,
1155
+ "val_cos": 0.94064,
1156
+ "key_mse": 0.18716,
1157
+ "val_mse": 0.00112,
1158
+ "attn_cos": 0.99336,
1159
+ "attn_output_error": 0.1609,
1160
+ "ip_rel": 4.07001,
1161
+ "ip_bias": 8e-05,
1162
+ "task": "repobench-p"
1163
+ },
1164
+ {
1165
+ "config": "turbo-mse-1b",
1166
+ "bits_per_elt": 1.125,
1167
+ "n_traces": 50,
1168
+ "key_cos": 0.79966,
1169
+ "val_cos": 0.79959,
1170
+ "key_mse": 0.58273,
1171
+ "val_mse": 0.00347,
1172
+ "attn_cos": 0.96834,
1173
+ "attn_output_error": 0.40316,
1174
+ "ip_rel": 4.66947,
1175
+ "ip_bias": -0.00025,
1176
+ "task": "repobench-p"
1177
+ },
1178
+ {
1179
+ "config": "turbo-prod-1b (K:+qjl)",
1180
+ "bits_per_elt": 2.125,
1181
+ "n_traces": 50,
1182
+ "key_cos": 0.79965,
1183
+ "val_cos": 0.79959,
1184
+ "key_mse": 0.58274,
1185
+ "val_mse": 0.00347,
1186
+ "attn_cos": 0.96834,
1187
+ "attn_output_error": 0.40325,
1188
+ "ip_rel": 7.08782,
1189
+ "ip_bias": -0.00021,
1190
+ "task": "repobench-p"
1191
+ }
1192
+ ]
artifacts/longbench_results.json ADDED
@@ -0,0 +1,632 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "task": "qasper",
4
+ "config": "fp16",
5
+ "bpe": 16.0,
6
+ "f1": 0.3584,
7
+ "delta_f1": 0.0
8
+ },
9
+ {
10
+ "task": "qasper",
11
+ "config": "scalar-int4",
12
+ "bpe": 4.25,
13
+ "f1": 0.3565,
14
+ "delta_f1": 0.0031
15
+ },
16
+ {
17
+ "task": "qasper",
18
+ "config": "scalar-int2",
19
+ "bpe": 2.25,
20
+ "f1": 0.2531,
21
+ "delta_f1": -0.1003
22
+ },
23
+ {
24
+ "task": "qasper",
25
+ "config": "kivi-int2",
26
+ "bpe": 2.25,
27
+ "f1": 0.27,
28
+ "delta_f1": -0.0834
29
+ },
30
+ {
31
+ "task": "qasper",
32
+ "config": "turboquant~-int2",
33
+ "bpe": 2.125,
34
+ "f1": 0.0927,
35
+ "delta_f1": -0.2607
36
+ },
37
+ {
38
+ "task": "qasper",
39
+ "config": "productvq-16x256-1b",
40
+ "bpe": 1.0,
41
+ "f1": 0.2049,
42
+ "delta_f1": -0.1485
43
+ },
44
+ {
45
+ "task": "qasper",
46
+ "config": "productvq-8x256-0.5b",
47
+ "bpe": 0.5,
48
+ "f1": 0.146,
49
+ "delta_f1": -0.2074
50
+ },
51
+ {
52
+ "task": "qasper",
53
+ "config": "ropesplit-1b",
54
+ "bpe": 1.0,
55
+ "f1": 0.2108,
56
+ "delta_f1": -0.1426
57
+ },
58
+ {
59
+ "task": "qasper",
60
+ "config": "sign-1bit",
61
+ "bpe": 1.125,
62
+ "f1": 0.1731,
63
+ "delta_f1": -0.1803
64
+ },
65
+ {
66
+ "task": "qasper",
67
+ "config": "ternary-bitnet",
68
+ "bpe": 1.71,
69
+ "f1": 0.221,
70
+ "delta_f1": -0.1325
71
+ },
72
+ {
73
+ "task": "qasper",
74
+ "config": "productvq-64x256-4b",
75
+ "bpe": 4.0,
76
+ "f1": 0.349,
77
+ "delta_f1": -0.0044
78
+ },
79
+ {
80
+ "task": "qasper",
81
+ "config": "productvq-32x256-2b",
82
+ "bpe": 2.0,
83
+ "f1": 0.3092,
84
+ "delta_f1": -0.0442
85
+ },
86
+ {
87
+ "task": "qasper",
88
+ "config": "turbo-mse-4b",
89
+ "bpe": 4.125,
90
+ "f1": 0.3582,
91
+ "delta_f1": 0.0048
92
+ },
93
+ {
94
+ "task": "qasper",
95
+ "config": "turbo-prod-4b (K:+qjl)",
96
+ "bpe": 5.125,
97
+ "f1": 0.3363,
98
+ "delta_f1": -0.0171
99
+ },
100
+ {
101
+ "task": "qasper",
102
+ "config": "turbo-mse-2b",
103
+ "bpe": 2.125,
104
+ "f1": 0.2605,
105
+ "delta_f1": -0.0929
106
+ },
107
+ {
108
+ "task": "qasper",
109
+ "config": "turbo-prod-2b (K:+qjl)",
110
+ "bpe": 3.125,
111
+ "f1": 0.2684,
112
+ "delta_f1": -0.085
113
+ },
114
+ {
115
+ "task": "qasper",
116
+ "config": "turbo-mse-1b",
117
+ "bpe": 1.125,
118
+ "f1": 0.1645,
119
+ "delta_f1": -0.1889
120
+ },
121
+ {
122
+ "task": "qasper",
123
+ "config": "turbo-prod-1b (K:+qjl)",
124
+ "bpe": 2.125,
125
+ "f1": 0.188,
126
+ "delta_f1": -0.1654
127
+ },
128
+ {
129
+ "task": "2wikimqa",
130
+ "config": "fp16",
131
+ "bpe": 16.0,
132
+ "f1": 0.3163,
133
+ "delta_f1": 0.0
134
+ },
135
+ {
136
+ "task": "2wikimqa",
137
+ "config": "scalar-int4",
138
+ "bpe": 4.25,
139
+ "f1": 0.3041,
140
+ "delta_f1": -0.0121
141
+ },
142
+ {
143
+ "task": "2wikimqa",
144
+ "config": "scalar-int2",
145
+ "bpe": 2.25,
146
+ "f1": 0.2227,
147
+ "delta_f1": -0.0936
148
+ },
149
+ {
150
+ "task": "2wikimqa",
151
+ "config": "kivi-int2",
152
+ "bpe": 2.25,
153
+ "f1": 0.1952,
154
+ "delta_f1": -0.1211
155
+ },
156
+ {
157
+ "task": "2wikimqa",
158
+ "config": "turboquant~-int2",
159
+ "bpe": 2.125,
160
+ "f1": 0.0392,
161
+ "delta_f1": -0.2771
162
+ },
163
+ {
164
+ "task": "2wikimqa",
165
+ "config": "productvq-16x256-1b",
166
+ "bpe": 1.0,
167
+ "f1": 0.2314,
168
+ "delta_f1": -0.0849
169
+ },
170
+ {
171
+ "task": "2wikimqa",
172
+ "config": "productvq-8x256-0.5b",
173
+ "bpe": 0.5,
174
+ "f1": 0.0979,
175
+ "delta_f1": -0.2184
176
+ },
177
+ {
178
+ "task": "2wikimqa",
179
+ "config": "ropesplit-1b",
180
+ "bpe": 1.0,
181
+ "f1": 0.2485,
182
+ "delta_f1": -0.0678
183
+ },
184
+ {
185
+ "task": "2wikimqa",
186
+ "config": "sign-1bit",
187
+ "bpe": 1.125,
188
+ "f1": 0.1601,
189
+ "delta_f1": -0.1562
190
+ },
191
+ {
192
+ "task": "2wikimqa",
193
+ "config": "ternary-bitnet",
194
+ "bpe": 1.71,
195
+ "f1": 0.244,
196
+ "delta_f1": -0.0723
197
+ },
198
+ {
199
+ "task": "2wikimqa",
200
+ "config": "productvq-64x256-4b",
201
+ "bpe": 4.0,
202
+ "f1": 0.2542,
203
+ "delta_f1": -0.0621
204
+ },
205
+ {
206
+ "task": "2wikimqa",
207
+ "config": "productvq-32x256-2b",
208
+ "bpe": 2.0,
209
+ "f1": 0.3099,
210
+ "delta_f1": -0.0064
211
+ },
212
+ {
213
+ "task": "2wikimqa",
214
+ "config": "turbo-mse-4b",
215
+ "bpe": 4.125,
216
+ "f1": 0.301,
217
+ "delta_f1": -0.0152
218
+ },
219
+ {
220
+ "task": "2wikimqa",
221
+ "config": "turbo-prod-4b (K:+qjl)",
222
+ "bpe": 5.125,
223
+ "f1": 0.3204,
224
+ "delta_f1": 0.0041
225
+ },
226
+ {
227
+ "task": "2wikimqa",
228
+ "config": "turbo-mse-2b",
229
+ "bpe": 2.125,
230
+ "f1": 0.2739,
231
+ "delta_f1": -0.0424
232
+ },
233
+ {
234
+ "task": "2wikimqa",
235
+ "config": "turbo-prod-2b (K:+qjl)",
236
+ "bpe": 3.125,
237
+ "f1": 0.2502,
238
+ "delta_f1": -0.0661
239
+ },
240
+ {
241
+ "task": "2wikimqa",
242
+ "config": "turbo-mse-1b",
243
+ "bpe": 1.125,
244
+ "f1": 0.2113,
245
+ "delta_f1": -0.105
246
+ },
247
+ {
248
+ "task": "2wikimqa",
249
+ "config": "turbo-prod-1b (K:+qjl)",
250
+ "bpe": 2.125,
251
+ "f1": 0.1891,
252
+ "delta_f1": -0.1272
253
+ },
254
+ {
255
+ "task": "hotpotqa",
256
+ "config": "fp16",
257
+ "bpe": 16.0,
258
+ "f1": 0.4018,
259
+ "delta_f1": 0.0
260
+ },
261
+ {
262
+ "task": "hotpotqa",
263
+ "config": "scalar-int4",
264
+ "bpe": 4.25,
265
+ "f1": 0.3873,
266
+ "delta_f1": 0.0155
267
+ },
268
+ {
269
+ "task": "hotpotqa",
270
+ "config": "scalar-int2",
271
+ "bpe": 2.25,
272
+ "f1": 0.2828,
273
+ "delta_f1": -0.089
274
+ },
275
+ {
276
+ "task": "hotpotqa",
277
+ "config": "kivi-int2",
278
+ "bpe": 2.25,
279
+ "f1": 0.2158,
280
+ "delta_f1": -0.156
281
+ },
282
+ {
283
+ "task": "hotpotqa",
284
+ "config": "turboquant~-int2",
285
+ "bpe": 2.125,
286
+ "f1": 0.0118,
287
+ "delta_f1": -0.36
288
+ },
289
+ {
290
+ "task": "hotpotqa",
291
+ "config": "productvq-16x256-1b",
292
+ "bpe": 1.0,
293
+ "f1": 0.1762,
294
+ "delta_f1": -0.1956
295
+ },
296
+ {
297
+ "task": "hotpotqa",
298
+ "config": "productvq-8x256-0.5b",
299
+ "bpe": 0.5,
300
+ "f1": 0.1524,
301
+ "delta_f1": -0.2194
302
+ },
303
+ {
304
+ "task": "hotpotqa",
305
+ "config": "ropesplit-1b",
306
+ "bpe": 1.0,
307
+ "f1": 0.2318,
308
+ "delta_f1": -0.14
309
+ },
310
+ {
311
+ "task": "hotpotqa",
312
+ "config": "sign-1bit",
313
+ "bpe": 1.125,
314
+ "f1": 0.1167,
315
+ "delta_f1": -0.2551
316
+ },
317
+ {
318
+ "task": "hotpotqa",
319
+ "config": "ternary-bitnet",
320
+ "bpe": 1.71,
321
+ "f1": 0.3327,
322
+ "delta_f1": -0.0391
323
+ },
324
+ {
325
+ "task": "hotpotqa",
326
+ "config": "productvq-64x256-4b",
327
+ "bpe": 4.0,
328
+ "f1": 0.4212,
329
+ "delta_f1": 0.0494
330
+ },
331
+ {
332
+ "task": "hotpotqa",
333
+ "config": "productvq-32x256-2b",
334
+ "bpe": 2.0,
335
+ "f1": 0.3679,
336
+ "delta_f1": -0.0039
337
+ },
338
+ {
339
+ "task": "hotpotqa",
340
+ "config": "turbo-mse-4b",
341
+ "bpe": 4.125,
342
+ "f1": 0.4002,
343
+ "delta_f1": 0.0284
344
+ },
345
+ {
346
+ "task": "hotpotqa",
347
+ "config": "turbo-prod-4b (K:+qjl)",
348
+ "bpe": 5.125,
349
+ "f1": 0.3697,
350
+ "delta_f1": -0.0021
351
+ },
352
+ {
353
+ "task": "hotpotqa",
354
+ "config": "turbo-mse-2b",
355
+ "bpe": 2.125,
356
+ "f1": 0.2987,
357
+ "delta_f1": -0.0731
358
+ },
359
+ {
360
+ "task": "hotpotqa",
361
+ "config": "turbo-prod-2b (K:+qjl)",
362
+ "bpe": 3.125,
363
+ "f1": 0.3223,
364
+ "delta_f1": -0.0495
365
+ },
366
+ {
367
+ "task": "hotpotqa",
368
+ "config": "turbo-mse-1b",
369
+ "bpe": 1.125,
370
+ "f1": 0.1526,
371
+ "delta_f1": -0.2192
372
+ },
373
+ {
374
+ "task": "hotpotqa",
375
+ "config": "turbo-prod-1b (K:+qjl)",
376
+ "bpe": 2.125,
377
+ "f1": 0.1677,
378
+ "delta_f1": -0.2041
379
+ },
380
+ {
381
+ "task": "passage_retrieval_en",
382
+ "config": "fp16",
383
+ "bpe": 16.0,
384
+ "f1": 1.0,
385
+ "delta_f1": 0.0
386
+ },
387
+ {
388
+ "task": "passage_retrieval_en",
389
+ "config": "scalar-int4",
390
+ "bpe": 4.25,
391
+ "f1": 0.98,
392
+ "delta_f1": -0.02
393
+ },
394
+ {
395
+ "task": "passage_retrieval_en",
396
+ "config": "scalar-int2",
397
+ "bpe": 2.25,
398
+ "f1": 0.74,
399
+ "delta_f1": -0.26
400
+ },
401
+ {
402
+ "task": "passage_retrieval_en",
403
+ "config": "kivi-int2",
404
+ "bpe": 2.25,
405
+ "f1": 0.62,
406
+ "delta_f1": -0.38
407
+ },
408
+ {
409
+ "task": "passage_retrieval_en",
410
+ "config": "turboquant~-int2",
411
+ "bpe": 2.125,
412
+ "f1": 0.02,
413
+ "delta_f1": -0.98
414
+ },
415
+ {
416
+ "task": "passage_retrieval_en",
417
+ "config": "productvq-16x256-1b",
418
+ "bpe": 1.0,
419
+ "f1": 0.26,
420
+ "delta_f1": -0.74
421
+ },
422
+ {
423
+ "task": "passage_retrieval_en",
424
+ "config": "productvq-8x256-0.5b",
425
+ "bpe": 0.5,
426
+ "f1": 0.06,
427
+ "delta_f1": -0.94
428
+ },
429
+ {
430
+ "task": "passage_retrieval_en",
431
+ "config": "ropesplit-1b",
432
+ "bpe": 1.0,
433
+ "f1": 0.1,
434
+ "delta_f1": -0.9
435
+ },
436
+ {
437
+ "task": "passage_retrieval_en",
438
+ "config": "sign-1bit",
439
+ "bpe": 1.125,
440
+ "f1": 0.08,
441
+ "delta_f1": -0.92
442
+ },
443
+ {
444
+ "task": "passage_retrieval_en",
445
+ "config": "ternary-bitnet",
446
+ "bpe": 1.71,
447
+ "f1": 0.26,
448
+ "delta_f1": -0.74
449
+ },
450
+ {
451
+ "task": "passage_retrieval_en",
452
+ "config": "productvq-64x256-4b",
453
+ "bpe": 4.0,
454
+ "f1": 1.0,
455
+ "delta_f1": 0.0
456
+ },
457
+ {
458
+ "task": "passage_retrieval_en",
459
+ "config": "productvq-32x256-2b",
460
+ "bpe": 2.0,
461
+ "f1": 0.72,
462
+ "delta_f1": -0.28
463
+ },
464
+ {
465
+ "task": "passage_retrieval_en",
466
+ "config": "turbo-mse-4b",
467
+ "bpe": 4.125,
468
+ "f1": 1.0,
469
+ "delta_f1": 0.0
470
+ },
471
+ {
472
+ "task": "passage_retrieval_en",
473
+ "config": "turbo-prod-4b (K:+qjl)",
474
+ "bpe": 5.125,
475
+ "f1": 1.0,
476
+ "delta_f1": 0.0
477
+ },
478
+ {
479
+ "task": "passage_retrieval_en",
480
+ "config": "turbo-mse-2b",
481
+ "bpe": 2.125,
482
+ "f1": 0.84,
483
+ "delta_f1": -0.16
484
+ },
485
+ {
486
+ "task": "passage_retrieval_en",
487
+ "config": "turbo-prod-2b (K:+qjl)",
488
+ "bpe": 3.125,
489
+ "f1": 0.9,
490
+ "delta_f1": -0.1
491
+ },
492
+ {
493
+ "task": "passage_retrieval_en",
494
+ "config": "turbo-mse-1b",
495
+ "bpe": 1.125,
496
+ "f1": 0.06,
497
+ "delta_f1": -0.94
498
+ },
499
+ {
500
+ "task": "passage_retrieval_en",
501
+ "config": "turbo-prod-1b (K:+qjl)",
502
+ "bpe": 2.125,
503
+ "f1": 0.02,
504
+ "delta_f1": -0.98
505
+ },
506
+ {
507
+ "task": "repobench-p",
508
+ "config": "fp16",
509
+ "bpe": 16.0,
510
+ "f1": 0.196,
511
+ "delta_f1": 0.0
512
+ },
513
+ {
514
+ "task": "repobench-p",
515
+ "config": "scalar-int4",
516
+ "bpe": 4.25,
517
+ "f1": 0.1949,
518
+ "delta_f1": 0.0089
519
+ },
520
+ {
521
+ "task": "repobench-p",
522
+ "config": "scalar-int2",
523
+ "bpe": 2.25,
524
+ "f1": 0.1671,
525
+ "delta_f1": -0.0189
526
+ },
527
+ {
528
+ "task": "repobench-p",
529
+ "config": "kivi-int2",
530
+ "bpe": 2.25,
531
+ "f1": 0.1744,
532
+ "delta_f1": -0.0116
533
+ },
534
+ {
535
+ "task": "repobench-p",
536
+ "config": "turboquant~-int2",
537
+ "bpe": 2.125,
538
+ "f1": 0.0757,
539
+ "delta_f1": -0.1103
540
+ },
541
+ {
542
+ "task": "repobench-p",
543
+ "config": "productvq-16x256-1b",
544
+ "bpe": 1.0,
545
+ "f1": 0.1693,
546
+ "delta_f1": -0.0167
547
+ },
548
+ {
549
+ "task": "repobench-p",
550
+ "config": "productvq-8x256-0.5b",
551
+ "bpe": 0.5,
552
+ "f1": 0.1483,
553
+ "delta_f1": -0.0376
554
+ },
555
+ {
556
+ "task": "repobench-p",
557
+ "config": "ropesplit-1b",
558
+ "bpe": 1.0,
559
+ "f1": 0.1618,
560
+ "delta_f1": -0.0241
561
+ },
562
+ {
563
+ "task": "repobench-p",
564
+ "config": "sign-1bit",
565
+ "bpe": 1.125,
566
+ "f1": 0.1309,
567
+ "delta_f1": -0.0551
568
+ },
569
+ {
570
+ "task": "repobench-p",
571
+ "config": "ternary-bitnet",
572
+ "bpe": 1.71,
573
+ "f1": 0.1675,
574
+ "delta_f1": -0.0185
575
+ },
576
+ {
577
+ "task": "repobench-p",
578
+ "config": "productvq-64x256-4b",
579
+ "bpe": 4.0,
580
+ "f1": 0.1891,
581
+ "delta_f1": 0.0032
582
+ },
583
+ {
584
+ "task": "repobench-p",
585
+ "config": "productvq-32x256-2b",
586
+ "bpe": 2.0,
587
+ "f1": 0.192,
588
+ "delta_f1": 0.0061
589
+ },
590
+ {
591
+ "task": "repobench-p",
592
+ "config": "turbo-mse-4b",
593
+ "bpe": 4.125,
594
+ "f1": 0.1854,
595
+ "delta_f1": -0.0005
596
+ },
597
+ {
598
+ "task": "repobench-p",
599
+ "config": "turbo-prod-4b (K:+qjl)",
600
+ "bpe": 5.125,
601
+ "f1": 0.1832,
602
+ "delta_f1": -0.0028
603
+ },
604
+ {
605
+ "task": "repobench-p",
606
+ "config": "turbo-mse-2b",
607
+ "bpe": 2.125,
608
+ "f1": 0.1874,
609
+ "delta_f1": 0.0015
610
+ },
611
+ {
612
+ "task": "repobench-p",
613
+ "config": "turbo-prod-2b (K:+qjl)",
614
+ "bpe": 3.125,
615
+ "f1": 0.193,
616
+ "delta_f1": 0.007
617
+ },
618
+ {
619
+ "task": "repobench-p",
620
+ "config": "turbo-mse-1b",
621
+ "bpe": 1.125,
622
+ "f1": 0.1579,
623
+ "delta_f1": -0.0281
624
+ },
625
+ {
626
+ "task": "repobench-p",
627
+ "config": "turbo-prod-1b (K:+qjl)",
628
+ "bpe": 2.125,
629
+ "f1": 0.1571,
630
+ "delta_f1": -0.0289
631
+ }
632
+ ]
artifacts/turbo_codebooks.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f6167b29f4be685cb01d41aab6443f7b0e491512a05f729bfeb14d46de721fde
3
+ size 9917315
benchmark.py ADDED
@@ -0,0 +1,782 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ benchmark.py — fit AttnVQ codebooks and evaluate on real Laguna-XS.2 caches.
3
+
4
+ Stages:
5
+ dump capture post-RoPE K/V from full-attention layers
6
+ fit per-layer LBG codebooks → artifacts/codebooks.pt
7
+ cheap proxy metrics (key cosine, attn-output error, ip-bias)
8
+ swebench optional resolve-rate delta on SWE-bench Verified (needs Docker)
9
+
10
+ Usage:
11
+ python benchmark.py --stage fit
12
+ python benchmark.py --stage cheap --n_eval 64
13
+ python benchmark.py --stage dump
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import os
21
+ import time
22
+ from concurrent.futures import ThreadPoolExecutor
23
+ from dataclasses import dataclass, asdict
24
+
25
+ import torch
26
+
27
+ from vqkv.quantizers import (ScalarKV, KIVIScalarKV, ProductVQKV, RoPESplitVQKV,
28
+ SignScalarKV, TernaryScalarKV)
29
+ from vqkv.metrics import (key_cosine, cache_mse, inner_product_distortion,
30
+ attention_output, attn_output_cosine, attn_output_error)
31
+
32
+ MODEL_ID = os.environ.get("LAGUNA_ID", "poolside/Laguna-XS.2")
33
+ ARTIFACT_DIR = os.environ.get("VQKV_ARTIFACTS", "./artifacts")
34
+ os.makedirs(ARTIFACT_DIR, exist_ok=True)
35
+
36
+ CALIB_DATASET = os.environ.get("CALIB_DATASET", "SWE-bench/SWE-smith-trajectories")
37
+ CALIB_SPLIT = os.environ.get("CALIB_SPLIT", "tool")
38
+ CALIB_SOURCE = os.environ.get("CALIB_SOURCE", "swesmith") # swesmith | longbench-hotpotqa
39
+
40
+ _HOTPOTQA_PROMPT = (
41
+ "Answer the question based on the given passages. "
42
+ "Only give me the answer and do not output any other words.\n\n"
43
+ "The following are given passages.\n{context}\n\n"
44
+ "Answer the question based on the given passages. "
45
+ "Only give me the answer and do not output any other words.\n\n"
46
+ "Question: {input}\nAnswer:"
47
+ )
48
+
49
+ # ============================================================================
50
+ # Cache configurations under test. Each is a (name, factory) where factory()
51
+ # returns an unfitted quantizer. `None` means the fp16 baseline (no quant).
52
+ # ============================================================================
53
+ def cache_configs():
54
+ return [
55
+ ("fp16 (baseline)", None),
56
+ ("scalar-int4", lambda: ScalarKV(nbits=4)),
57
+ ("scalar-int2", lambda: ScalarKV(nbits=2)),
58
+ ("kivi-int2", lambda: KIVIScalarKV(nbits=2)),
59
+ ("productvq-64x256-4b", lambda: ProductVQKV(n_sub=64, n_codes=256, iters=15)),
60
+ ("productvq-32x256-2b", lambda: ProductVQKV(n_sub=32, n_codes=256, iters=15)),
61
+ ("productvq-16x256-1b", lambda: ProductVQKV(n_sub=16, n_codes=256, iters=15)),
62
+ ("productvq-8x256-0.5b", lambda: ProductVQKV(n_sub=8, n_codes=256, iters=15)),
63
+ ("ropesplit-1b", lambda: RoPESplitVQKV(n_sub_half=8, n_codes=256, iters=15)),
64
+ ("sign-1bit", lambda: SignScalarKV(per_channel_key=True)),
65
+ ("ternary-bitnet", lambda: TernaryScalarKV(alpha=0.7, per_channel_key=True)),
66
+ ]
67
+
68
+
69
+ # ============================================================================
70
+ # Model + layer-structure loading
71
+ # ============================================================================
72
+ def load_model_and_meta():
73
+ from transformers import AutoModelForCausalLM, AutoTokenizer
74
+ tok = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
75
+ model = AutoModelForCausalLM.from_pretrained(
76
+ MODEL_ID, torch_dtype=torch.bfloat16, device_map="cuda",
77
+ trust_remote_code=True)
78
+ model.eval()
79
+ cfg = model.config
80
+ full_layers = [i for i, t in enumerate(cfg.layer_types) if t == "full_attention"]
81
+ meta = {
82
+ "full_layers": full_layers,
83
+ "n_kv_heads": cfg.num_key_value_heads,
84
+ "n_q_heads": cfg.num_attention_heads,
85
+ "head_dim": cfg.head_dim,
86
+ "n_layers": cfg.num_hidden_layers,
87
+ }
88
+ print(f"[meta] full-attention layers ({len(full_layers)}): {full_layers}")
89
+ print(f"[meta] kv_heads={meta['n_kv_heads']} head_dim={meta['head_dim']}")
90
+ assert len(full_layers) > 0, "no full-attention layers found; check config"
91
+ return model, tok, meta
92
+
93
+
94
+ # Trace flattening (SWE-smith messages JSON, nebius trajectory/tool roles).
95
+ def flatten_trace(example, tok) -> str:
96
+ """Flatten one trajectory to a string via the model's chat template."""
97
+ raw = (example.get("messages") or example.get("trajectory")
98
+ or example.get("conversations"))
99
+ if raw is None:
100
+ return json.dumps(example)[:200_000]
101
+
102
+ # SWE-smith stores 'messages' as a JSON string, not a native list.
103
+ if isinstance(raw, str):
104
+ try:
105
+ raw = json.loads(raw)
106
+ except json.JSONDecodeError:
107
+ return raw[:200_000]
108
+
109
+ norm = []
110
+ for m in raw:
111
+ role = m.get("role") or m.get("from") or "user"
112
+ content = m.get("content") or m.get("value") or ""
113
+
114
+ # SWE-smith: content can be a list of {"type":"text","text":"..."} blocks
115
+ if isinstance(content, list):
116
+ content = "\n".join(
117
+ item.get("text", str(item)) if isinstance(item, dict) else str(item)
118
+ for item in content
119
+ )
120
+
121
+ # nebius: assistant turns carry tool_calls alongside content
122
+ tool_calls = m.get("tool_calls")
123
+ if tool_calls:
124
+ tc_text = json.dumps(tool_calls, ensure_ascii=False)
125
+ content = (content + "\n" + tc_text).strip() if content else tc_text
126
+
127
+ # 'tool' role (observation) has no equivalent in most chat templates;
128
+ # map it to 'user' so the template accepts it.
129
+ role = {"human": "user", "gpt": "assistant", "tool": "user"}.get(role, role)
130
+
131
+ if not content.strip():
132
+ continue
133
+ norm.append({"role": role, "content": content})
134
+
135
+ # Merge consecutive same-role messages produced by tool->user collapsing.
136
+ merged: list[dict] = []
137
+ for m in norm:
138
+ if merged and merged[-1]["role"] == m["role"]:
139
+ merged[-1]["content"] += "\n\n" + m["content"]
140
+ else:
141
+ merged.append(dict(m))
142
+
143
+ try:
144
+ return tok.apply_chat_template(merged, tokenize=False,
145
+ add_generation_prompt=False)
146
+ except Exception:
147
+ return "\n\n".join(f"{m['role']}: {m['content']}" for m in merged)
148
+
149
+
150
+ def flatten_longbench(example) -> str:
151
+ """Format a LongBench hotpotqa example (context + input) as a plain string."""
152
+ return _HOTPOTQA_PROMPT.format(
153
+ context=example["context"], input=example["input"])
154
+
155
+
156
+ def _load_longbench_hotpotqa():
157
+ """Load THUDM/LongBench hotpotqa, bypassing the deprecated dataset script."""
158
+ from datasets import load_dataset as _ld
159
+ for fname in ("hotpotqa_e.jsonl", "hotpotqa.jsonl"):
160
+ try:
161
+ return _ld(
162
+ "json",
163
+ data_files=f"hf://datasets/THUDM/LongBench/data/{fname}",
164
+ split="train",
165
+ )
166
+ except Exception:
167
+ continue
168
+ return _ld("THUDM/LongBench", name="hotpotqa", split="test")
169
+
170
+
171
+ def _load_calib_dump_dataset(n_calib: int, calib_source: str, tok):
172
+ """Return (dataset, text_fn, label) for stage_dump."""
173
+ from datasets import load_dataset
174
+
175
+ if calib_source == "longbench-hotpotqa":
176
+ ds = _load_longbench_hotpotqa()
177
+ n_total = len(ds)
178
+ # Last n_calib rows avoid overlap with longbench_eval.py (range(0, n_eval)).
179
+ start = max(0, n_total - n_calib)
180
+ ds = ds.select(range(start, n_total))
181
+ label = f"LongBench hotpotqa (rows {start}–{n_total - 1})"
182
+ return ds, flatten_longbench, label
183
+
184
+ ds = load_dataset(CALIB_DATASET, split=CALIB_SPLIT)
185
+ label = f"{CALIB_DATASET} split={CALIB_SPLIT}"
186
+ return ds, lambda ex: flatten_trace(ex, tok), label
187
+
188
+
189
+ # STAGE: dump -- run real traces, capture post-RoPE K/V from full-attn layers
190
+ def stage_dump(n_calib=16, max_len=32768, calib_source: str | None = None,
191
+ min_len=2048):
192
+ from transformers.cache_utils import DynamicCache
193
+
194
+ calib_source = calib_source or CALIB_SOURCE
195
+ model, tok, meta = load_model_and_meta()
196
+ full = set(meta["full_layers"])
197
+
198
+ # device_map="auto" can leave model.device as 'meta'; always use cuda:0.
199
+ input_device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
200
+
201
+ class DumpingCache(DynamicCache):
202
+ def __init__(self, *a, **k):
203
+ super().__init__(*a, **k)
204
+ self.dump = {i: {"k": [], "v": []} for i in full}
205
+ def update(self, key_states, value_states, layer_idx, cache_kwargs=None):
206
+ if layer_idx in full:
207
+ self.dump[layer_idx]["k"].append(
208
+ key_states.detach()[0].permute(1, 0, 2).float().cpu())
209
+ self.dump[layer_idx]["v"].append(
210
+ value_states.detach()[0].permute(1, 0, 2).float().cpu())
211
+ return super().update(key_states, value_states, layer_idx, cache_kwargs)
212
+
213
+ ds, get_text, source_label = _load_calib_dump_dataset(n_calib, calib_source, tok)
214
+ print(f"[dump] source={calib_source} {source_label} rows={len(ds)} "
215
+ f"schema={list(ds[0].keys())}")
216
+
217
+ agg = {i: {"k": [], "v": []} for i in full}
218
+ used = 0
219
+ for ex in ds:
220
+ if used >= n_calib:
221
+ break
222
+ text = get_text(ex)
223
+ ids = tok(text, return_tensors="pt", truncation=True,
224
+ max_length=max_len).to(input_device)
225
+ if ids["input_ids"].shape[1] < min_len:
226
+ continue
227
+ cache = DumpingCache(config=model.config) # fresh per trace
228
+ with torch.no_grad():
229
+ model.model(**ids, past_key_values=cache, use_cache=True) # skip lm_head
230
+ for i in full:
231
+ agg[i]["k"].append(torch.cat(cache.dump[i]["k"]))
232
+ agg[i]["v"].append(torch.cat(cache.dump[i]["v"]))
233
+ used += 1
234
+ print(f"[dump] trace {used}/{n_calib} len={ids['input_ids'].shape[1]}")
235
+
236
+ calib = {i: {"k": torch.cat(agg[i]["k"]), "v": torch.cat(agg[i]["v"])}
237
+ for i in full}
238
+ for i in list(full)[:3]:
239
+ k = calib[i]["k"].reshape(-1, meta["head_dim"])
240
+ cut = meta["head_dim"] // 2
241
+ print(f"[dump] layer {i}: {tuple(calib[i]['k'].shape)} | "
242
+ f"rope-half std {k[:, :cut].std():.3f} "
243
+ f"pass-half std {k[:, cut:].std():.3f} "
244
+ f"max|ch| {k.abs().amax(0).max():.2f}")
245
+ path = os.path.join(ARTIFACT_DIR, "calib_caches.pt")
246
+ torch.save({"calib": calib, "meta": meta}, path)
247
+ print(f"[dump] saved -> {path}")
248
+
249
+
250
+ # STAGE: fit -- per-layer codebooks for every (data-dependent) config
251
+ def stage_fit(only: list[str] | None = None):
252
+ """Fit quantizers and write artifacts/codebooks.pt.
253
+
254
+ By default fits every config in cache_configs(). Pass ``only=["sign-1bit", ...]``
255
+ to fit just those names and merge into an existing codebooks.pt (skips the rest).
256
+ Tuning-free quantizers (Sign, Ternary, Scalar, KIVI) finish in seconds; only
257
+ ProductVQ / RoPE-split need calib_caches.pt.
258
+ """
259
+ codebooks_path = os.path.join(ARTIFACT_DIR, "codebooks.pt")
260
+ calib_path = os.path.join(ARTIFACT_DIR, "calib_caches.pt")
261
+
262
+ if only and os.path.exists(codebooks_path):
263
+ existing = torch.load(codebooks_path, weights_only=False)
264
+ fitted = existing["fitted"]
265
+ meta = existing["meta"]
266
+ print(f"[fit] merging into existing {codebooks_path} ({len(fitted)} configs)")
267
+ else:
268
+ if not os.path.exists(calib_path):
269
+ raise FileNotFoundError(
270
+ f"{calib_path} not found; run --stage dump first, or use "
271
+ f"--only with an existing codebooks.pt for tuning-free configs")
272
+ blob = torch.load(calib_path)
273
+ calib, meta = blob["calib"], blob["meta"]
274
+ fitted = {}
275
+
276
+ calib = None
277
+ if os.path.exists(calib_path):
278
+ calib = torch.load(calib_path)["calib"]
279
+
280
+ hd = meta["head_dim"]
281
+ layer_ids = meta["full_layers"]
282
+
283
+ configs = [(n, f) for n, f in cache_configs() if f is not None]
284
+ if only is not None:
285
+ only_set = set(only)
286
+ configs = [(n, f) for n, f in configs if n in only_set]
287
+ unknown = only_set - {n for n, _ in configs}
288
+ if unknown:
289
+ raise ValueError(f"unknown --only config(s): {sorted(unknown)}")
290
+
291
+ # GPU fitting: LBG is pure torch; moving calib to CUDA makes bmm/argmin ~20x
292
+ # faster. Serial over layers on GPU (CUDA is already async; threading adds no
293
+ # benefit). Parallel over layers on CPU (BLAS releases GIL; real concurrency).
294
+ fit_device = "cuda" if torch.cuda.is_available() else "cpu"
295
+ if fit_device == "cuda":
296
+ print(f"[fit] GPU available ({torch.cuda.get_device_name()}) -- fitting LBG on CUDA")
297
+ n_workers = 1 if fit_device == "cuda" else min(len(layer_ids), os.cpu_count() or 1)
298
+
299
+ for name, factory in configs:
300
+ t0 = time.time()
301
+
302
+ def _fit_layer(i, _factory=factory, _device=fit_device):
303
+ if calib is not None and i in calib:
304
+ kf = calib[i]["k"].reshape(-1, hd)[:200_000].to(_device)
305
+ vf = calib[i]["v"].reshape(-1, hd)[:200_000].to(_device)
306
+ else:
307
+ kf = torch.zeros(1, hd, device=_device)
308
+ vf = torch.zeros(1, hd, device=_device)
309
+ q = _factory().fit(kf, vf)
310
+ # Codebooks are saved to disk as CPU tensors; move back before returning.
311
+ if _device != "cpu" and hasattr(q, "to"):
312
+ q.to("cpu")
313
+ return i, q
314
+
315
+ with ThreadPoolExecutor(max_workers=n_workers) as ex:
316
+ per_layer = dict(ex.map(_fit_layer, layer_ids))
317
+
318
+ fitted[name] = per_layer
319
+ print(f"[fit] {name}: {len(per_layer)} layer-codebooks in {time.time()-t0:.1f}s")
320
+
321
+ torch.save({"fitted": fitted, "meta": meta}, codebooks_path)
322
+ print(f"[fit] saved -> {codebooks_path} ({len(fitted)} configs total)")
323
+
324
+
325
+ # A drop-in cache that applies a per-layer quantizer to the target layers only.
326
+ def make_vq_cache_class(per_layer_quantizers, target_layers, model_config, device=None):
327
+ """Build a VQCache class with codebooks pre-moved to `device`.
328
+
329
+ Pass device="cuda" (or the model's device) so the roundtrip runs entirely
330
+ on GPU with no CPU<->GPU transfers. Without this, every update() call
331
+ implicitly transfers key_states to CPU and back, making decode very slow.
332
+ """
333
+ from transformers.cache_utils import DynamicCache
334
+
335
+ if device is not None:
336
+ for q in per_layer_quantizers.values():
337
+ if hasattr(q, "to"):
338
+ q.to(device)
339
+
340
+ class VQCache(DynamicCache):
341
+ def __init__(self, *a, **k):
342
+ super().__init__(*a, **k)
343
+ self.q = per_layer_quantizers
344
+ self.target = set(target_layers)
345
+ def update(self, key_states, value_states, layer_idx, cache_kwargs=None):
346
+ if layer_idx in self.target and layer_idx in self.q:
347
+ q = self.q[layer_idx]
348
+ b, h, s, d = key_states.shape
349
+ kf = key_states[0].transpose(0, 1).reshape(-1, d).float()
350
+ vf = value_states[0].transpose(0, 1).reshape(-1, d).float()
351
+ # See stage_cheap: per-channel-key quantizers reduce the key
352
+ # along the token axis and need a per-head block, not a flatten.
353
+ per_channel_key = (
354
+ isinstance(q, KIVIScalarKV)
355
+ or getattr(q, "per_channel_key", False)
356
+ )
357
+ if per_channel_key:
358
+ kk = key_states[0].permute(1, 0, 2).float() # (s, h, d)
359
+ k_hat = torch.stack([q.roundtrip_k(kk[:, hh, :]) for hh in range(h)], 1)
360
+ vv = value_states[0].permute(1, 0, 2).float()
361
+ v_hat = torch.stack([q.roundtrip_v(vv[:, hh, :]) for hh in range(h)], 1)
362
+ k_hat = k_hat.permute(1, 0, 2)[None]
363
+ v_hat = v_hat.permute(1, 0, 2)[None]
364
+ else:
365
+ k_hat = q.roundtrip_k(kf).reshape(s, h, d).permute(1, 0, 2)[None]
366
+ v_hat = q.roundtrip_v(vf).reshape(s, h, d).permute(1, 0, 2)[None]
367
+ key_states = k_hat.to(key_states.dtype).to(key_states.device)
368
+ value_states = v_hat.to(value_states.dtype).to(value_states.device)
369
+ return super().update(key_states, value_states, layer_idx, cache_kwargs)
370
+ return VQCache
371
+
372
+
373
+ # STAGE: cheap -- tier-1/2 metrics on held-out trace windows (no test suites)
374
+ def stage_cheap(n_eval=64, max_len=16384):
375
+ import collections
376
+ from datasets import load_dataset
377
+ blob = torch.load(os.path.join(ARTIFACT_DIR, "codebooks.pt"), weights_only=False)
378
+ fitted, meta = blob["fitted"], blob["meta"]
379
+ hd, full = meta["head_dim"], meta["full_layers"]
380
+ n_q = meta.get("n_q_heads", 48) # fallback for codebooks.pt written before this field
381
+
382
+ # Window for O(T²) attention metrics. 512 tokens keeps peak mem <200MB on GPU.
383
+ ATTN_WIN = 512
384
+
385
+ model, tok, _ = load_model_and_meta()
386
+ from tqdm import tqdm
387
+ from transformers.cache_utils import DynamicCache
388
+
389
+ # Move every fitted codebook to the model device ONCE. The roundtrip then
390
+ # runs entirely on-GPU against the on-GPU eval caches (see EvalDump below).
391
+ # ScalarKV/KIVI are tuning-free and have no .to(); they follow their input.
392
+ for per_layer in fitted.values():
393
+ for q in per_layer.values():
394
+ if hasattr(q, "to"):
395
+ q.to(model.device)
396
+
397
+ class EvalDump(DynamicCache):
398
+ def __init__(self):
399
+ super().__init__(); self.d = {i: {} for i in full}
400
+ def update(self, ks, vs, li, ck=None):
401
+ if li in set(full):
402
+ # Keep on-GPU: the quantizer roundtrip is a nearest-neighbour
403
+ # search -- a GPU job. Moving to CPU here dominated runtime.
404
+ self.d[li]["k"] = ks.detach()[0].permute(1, 0, 2).float()
405
+ self.d[li]["v"] = vs.detach()[0].permute(1, 0, 2).float()
406
+ return super().update(ks, vs, li, ck)
407
+
408
+ ds = load_dataset(CALIB_DATASET, split=CALIB_SPLIT)
409
+
410
+ # Per-trace rows (not saved to disk; aggregated below)
411
+ trace_rows = []
412
+
413
+ # Held-out slice. Start at 500 to safely clear any rows the dump stage
414
+ # consumed (dump skips short traces, so actual rows used >> n_calib=16).
415
+ for ex in tqdm(ds.select(range(500, 500 + n_eval))):
416
+ text = flatten_trace(ex, tok)
417
+ ids = tok(text, return_tensors="pt", truncation=True, max_length=max_len).to(model.device)
418
+ cache = EvalDump()
419
+ with torch.no_grad():
420
+ # model.model() skips lm_head: avoids a ~3 GB allocation per trace
421
+ # (max_len * vocab_size * 2 bytes) that is not needed for KV metrics.
422
+ model.model(**ids, past_key_values=cache, use_cache=True)
423
+
424
+ # Synthetic Q for attention/IP metrics: generated once per (trace, layer)
425
+ # and reused across all configs so the comparison is apples-to-apples.
426
+ # Unit-normalised so inner-product scale doesn't swamp the bias signal.
427
+ synth_q = {}
428
+ for i in full:
429
+ s = cache.d[i]["k"].shape[0]
430
+ win = min(s, ATTN_WIN)
431
+ q_rand = torch.randn(win, n_q, hd, device=cache.d[i]["k"].device)
432
+ synth_q[i] = q_rand / q_rand.norm(dim=-1, keepdim=True).clamp_min(1e-8)
433
+
434
+ for name, _ in cache_configs():
435
+ if name == "fp16 (baseline)":
436
+ continue
437
+ per_layer = fitted[name]
438
+ acc = collections.defaultdict(float)
439
+ nL = 0
440
+ for i in full:
441
+ k = cache.d[i]["k"] # (s, h, d)
442
+ v = cache.d[i]["v"]
443
+ q = per_layer[i]
444
+ s, h, d = k.shape
445
+
446
+ # Per-channel-key quantizers (KIVI, Sign, Ternary with
447
+ # per_channel_key=True) reduce the KEY along the TOKEN axis
448
+ # (dim=0). They must see one (s, d) block PER HEAD; flattening
449
+ # (s,h,d)->(s*h,d) would mix tokens across heads into one scale
450
+ # and corrupt the key metric. VQ/scalar-per-token quantizers
451
+ # reduce along dim=-1 and are safe to flatten.
452
+ per_channel_key = (
453
+ isinstance(q, KIVIScalarKV)
454
+ or getattr(q, "per_channel_key", False)
455
+ )
456
+ if per_channel_key:
457
+ k_hat = torch.stack([q.roundtrip_k(k[:, hh, :]) for hh in range(h)], 1)
458
+ v_hat = torch.stack([q.roundtrip_v(v[:, hh, :]) for hh in range(h)], 1)
459
+ else:
460
+ k_hat = q.roundtrip_k(k.reshape(-1, d)).reshape(s, h, d)
461
+ v_hat = q.roundtrip_v(v.reshape(-1, d)).reshape(s, h, d)
462
+
463
+ acc["key_cos"] += key_cosine(k, k_hat)
464
+ acc["val_cos"] += key_cosine(v, v_hat)
465
+ acc["key_mse"] += cache_mse(k, k_hat)
466
+ acc["val_mse"] += cache_mse(v, v_hat)
467
+
468
+ # Windowed attention/IP metrics on last ATTN_WIN tokens
469
+ win = min(s, ATTN_WIN)
470
+ kw, kw_hat = k[-win:], k_hat[-win:]
471
+ vw, vw_hat = v[-win:], v_hat[-win:]
472
+ q_syn = synth_q[i] # (win, n_q, d)
473
+
474
+ out_ref, _ = attention_output(q_syn, kw, vw, n_q)
475
+ out_hat, _ = attention_output(q_syn, kw_hat, vw_hat, n_q)
476
+ acc["attn_cos"] += attn_output_cosine(out_ref, out_hat)
477
+ acc["attn_output_error"] += attn_output_error(out_ref, out_hat)
478
+
479
+ ip = inner_product_distortion(q_syn, kw, kw_hat)
480
+ acc["ip_rel"] += ip["ip_rel_err"]
481
+ acc["ip_bias"] += ip["ip_bias"]
482
+
483
+ nL += 1
484
+
485
+ trace_rows.append({
486
+ "trace_len": ids["input_ids"].shape[1],
487
+ "config": name,
488
+ "key_cos": acc["key_cos"] / nL,
489
+ "val_cos": acc["val_cos"] / nL,
490
+ "key_mse": acc["key_mse"] / nL,
491
+ "val_mse": acc["val_mse"] / nL,
492
+ "attn_cos": acc["attn_cos"] / nL,
493
+ "attn_output_error": acc["attn_output_error"] / nL,
494
+ "ip_rel": acc["ip_rel"] / nL,
495
+ "ip_bias": acc["ip_bias"] / nL,
496
+ })
497
+
498
+ # Aggregate across traces: one summary row per config (what gets saved)
499
+ agg = collections.defaultdict(lambda: collections.defaultdict(list))
500
+ for r in trace_rows:
501
+ for col in ("key_cos", "val_cos", "key_mse", "val_mse",
502
+ "attn_cos", "attn_output_error", "ip_rel", "ip_bias"):
503
+ agg[r["config"]][col].append(r[col])
504
+
505
+ COLS = ("key_cos", "val_cos", "key_mse", "val_mse",
506
+ "attn_cos", "attn_output_error", "ip_rel", "ip_bias")
507
+
508
+ summary = []
509
+ for name, _ in cache_configs():
510
+ if name not in agg:
511
+ continue
512
+ cols = agg[name]
513
+ n = len(cols["key_cos"])
514
+ q0 = next(iter(fitted[name].values()))
515
+ row = {"config": name, "bits_per_elt": round(q0.bits_per_element(hd), 4), "n_traces": n}
516
+ for col in COLS:
517
+ row[col] = round(sum(cols[col]) / n, 5)
518
+ summary.append(row)
519
+
520
+ print(f"\n[cheap] mean metrics over {n_eval} held-out traces:")
521
+ print(f" {'config':24s} {'bpe':>5} {'key_cos':>8} {'val_cos':>8} "
522
+ f"{'key_mse':>9} {'val_mse':>9} {'attn_cos':>9} {'attn_err':>9} "
523
+ f"{'ip_rel':>8} {'ip_bias':>9}")
524
+ for row in summary:
525
+ print(f" {row['config']:24s} {row['bits_per_elt']:5.2f} "
526
+ f"{row['key_cos']:8.4f} {row['val_cos']:8.4f} "
527
+ f"{row['key_mse']:9.5f} {row['val_mse']:9.5f} "
528
+ f"{row['attn_cos']:9.4f} {row['attn_output_error']:9.4f} "
529
+ f"{row['ip_rel']:8.5f} {row['ip_bias']:9.6f}")
530
+
531
+ out_path = os.path.join(ARTIFACT_DIR, "cheap_metrics.json")
532
+ json.dump(summary, open(out_path, "w"), indent=2)
533
+ print(f"[cheap] saved -> {out_path}")
534
+
535
+
536
+ # Optional SWE-bench Verified eval (requires Docker + swebench).
537
+ _AGENT_SYSTEM = (
538
+ "You are an expert software engineer fixing a GitHub issue. "
539
+ "You have a bash shell inside the repository checked out at the failing commit. "
540
+ "Use <bash>command</bash> tags to run shell commands. "
541
+ "Explore the code, implement the fix, then output <submit> when done."
542
+ )
543
+
544
+
545
+ def _agent_loop(model, tok, task: dict, cache_factory, max_turns: int,
546
+ max_new: int = 1024) -> str:
547
+ """Run a minimal ReAct-bash loop on one SWE-bench task.
548
+
549
+ Clones the repo at base_commit into a temp dir, runs the model in a
550
+ generate→bash→observe loop, and returns the final `git diff HEAD` patch.
551
+ Each generate call rebuilds the full context from scratch so the VQCache
552
+ sees the compounding long-context pressure that the project targets.
553
+ """
554
+ import re
555
+ import shutil
556
+ import subprocess
557
+ import tempfile
558
+
559
+ repo_dir = tempfile.mkdtemp(prefix="sweagent_")
560
+ try:
561
+ subprocess.run(
562
+ ["git", "clone", f"https://github.com/{task['repo']}.git", repo_dir],
563
+ check=True, capture_output=True, timeout=120,
564
+ )
565
+ subprocess.run(
566
+ ["git", "checkout", task["base_commit"]],
567
+ check=True, capture_output=True, cwd=repo_dir, timeout=30,
568
+ )
569
+ except Exception as exc:
570
+ print(f" [agent] clone/checkout failed for {task['instance_id']}: {exc}")
571
+ shutil.rmtree(repo_dir, ignore_errors=True)
572
+ return ""
573
+
574
+ try:
575
+ messages = [
576
+ {"role": "system", "content": _AGENT_SYSTEM},
577
+ {"role": "user", "content": (
578
+ f"Repository: {task['repo']}\n\n"
579
+ f"Issue:\n{task['problem_statement']}"
580
+ )},
581
+ ]
582
+
583
+ for _ in range(max_turns):
584
+ prompt = tok.apply_chat_template(
585
+ messages, tokenize=False, add_generation_prompt=True)
586
+ ids = tok(prompt, return_tensors="pt", truncation=True,
587
+ max_length=32768).to(model.device)
588
+
589
+ cache = cache_factory()
590
+ with torch.no_grad():
591
+ out = model.generate(
592
+ **ids, max_new_tokens=max_new, do_sample=False,
593
+ past_key_values=cache, use_cache=True,
594
+ )
595
+ gen = tok.decode(out[0, ids["input_ids"].shape[1]:],
596
+ skip_special_tokens=True)
597
+ messages.append({"role": "assistant", "content": gen})
598
+
599
+ if re.search(r"<submit\s*/?>", gen, re.I):
600
+ break
601
+
602
+ cmds = re.findall(r"<bash>(.*?)</bash>", gen, re.DOTALL)
603
+ if not cmds:
604
+ break # model stopped issuing commands; take whatever diff we have
605
+
606
+ obs_parts = []
607
+ for cmd in cmds:
608
+ try:
609
+ r = subprocess.run(
610
+ cmd, shell=True, capture_output=True, text=True,
611
+ timeout=30, cwd=repo_dir,
612
+ )
613
+ obs_parts.append(
614
+ f"$ {cmd.strip()}\n{(r.stdout + r.stderr)[:2000]}")
615
+ except subprocess.TimeoutExpired:
616
+ obs_parts.append(f"$ {cmd.strip()}\n[timeout after 30s]")
617
+ messages.append({"role": "user", "content": "\n\n".join(obs_parts)})
618
+
619
+ diff = subprocess.run(
620
+ ["git", "diff", "HEAD"],
621
+ capture_output=True, text=True, cwd=repo_dir,
622
+ )
623
+ return diff.stdout
624
+ finally:
625
+ shutil.rmtree(repo_dir, ignore_errors=True)
626
+
627
+
628
+ def run_swebench_subset(model, tok, cache_factory_per_layer, target_layers,
629
+ task_ids, max_turns=100):
630
+ """Run the official SWE-bench Verified harness over `task_ids`.
631
+
632
+ Builds per-config VQCache (or None for fp16), runs the mini-bash agent on
633
+ each task, writes predictions.jsonl, evaluates with the swebench harness,
634
+ and returns resolve rate in [0, 1]. Keep `task_ids` FIXED across configs.
635
+ """
636
+ import glob
637
+ from datasets import load_dataset
638
+
639
+ # Build cache factory: VQCache for quantized configs, None for fp16.
640
+ if cache_factory_per_layer is not None:
641
+ VQCls = make_vq_cache_class(cache_factory_per_layer, target_layers,
642
+ model.config, device=model.device)
643
+ def cache_factory(): return VQCls()
644
+ else:
645
+ def cache_factory(): return None # model allocates DynamicCache internally
646
+
647
+ # Load task metadata indexed by instance_id.
648
+ verified = load_dataset("princeton-nlp/SWE-bench_Verified", split="test")
649
+ tasks = {r["instance_id"]: r for r in verified
650
+ if r["instance_id"] in set(task_ids)}
651
+
652
+ # Generate patches with the agent.
653
+ run_id = f"vqkv_{int(time.time())}"
654
+ predictions = []
655
+ for task_id in task_ids:
656
+ task = tasks.get(task_id)
657
+ if task is None:
658
+ print(f"[swebench] {task_id}: not in Verified dataset, skipping")
659
+ continue
660
+ print(f"[swebench] {task_id} ({len(predictions)+1}/{len(task_ids)})")
661
+ patch = _agent_loop(model, tok, task, cache_factory, max_turns=max_turns)
662
+ predictions.append({
663
+ "instance_id": task_id,
664
+ "model_patch": patch,
665
+ "model_name_or_path": "laguna-vqkv",
666
+ })
667
+
668
+ preds_path = os.path.join(ARTIFACT_DIR, f"{run_id}.jsonl")
669
+ with open(preds_path, "w") as f:
670
+ for p in predictions:
671
+ f.write(json.dumps(p) + "\n")
672
+ print(f"[swebench] wrote {len(predictions)} predictions -> {preds_path}")
673
+
674
+ # Run the official harness (needs Docker daemon).
675
+ # pip install swebench
676
+ from swebench.harness.run_evaluation import main as run_evaluation
677
+ run_evaluation(
678
+ dataset_name_or_path="princeton-nlp/SWE-bench_Verified",
679
+ split="test",
680
+ instance_ids=task_ids,
681
+ predictions_path=preds_path,
682
+ max_workers=4,
683
+ force_rebuild=False,
684
+ cache_level="env",
685
+ clean=False,
686
+ open_file_limit=4096,
687
+ run_id=run_id,
688
+ timeout=1800,
689
+ )
690
+
691
+ # Parse results. swebench writes a JSON summary; location varies by version.
692
+ result_files = (
693
+ glob.glob(os.path.join(ARTIFACT_DIR, f"{run_id}*.json"))
694
+ + glob.glob(f"{run_id}*.json") # also check cwd
695
+ )
696
+ if not result_files:
697
+ print(f"[swebench] WARNING: no results file found for run_id={run_id}")
698
+ return 0.0
699
+
700
+ with open(result_files[0]) as f:
701
+ results = json.load(f)
702
+
703
+ if isinstance(results, list):
704
+ n_resolved = sum(1 for r in results if r.get("resolved", False))
705
+ elif isinstance(results, dict):
706
+ # some harness versions use {instance_id: {resolved: bool, ...}}
707
+ n_resolved = sum(1 for v in results.values()
708
+ if (v.get("resolved") if isinstance(v, dict) else v))
709
+ else:
710
+ n_resolved = 0
711
+
712
+ return n_resolved / len(task_ids)
713
+
714
+
715
+ def stage_swebench(n_tasks=50, seed=0):
716
+ import random
717
+ from datasets import load_dataset
718
+ blob = torch.load(os.path.join(ARTIFACT_DIR, "codebooks.pt"))
719
+ fitted, meta = blob["fitted"], blob["meta"]
720
+ model, tok, _ = load_model_and_meta()
721
+
722
+ verified = load_dataset("princeton-nlp/SWE-bench_Verified", split="test")
723
+ rng = random.Random(seed)
724
+ task_ids = [verified[i]["instance_id"]
725
+ for i in rng.sample(range(len(verified)), n_tasks)]
726
+ json.dump(task_ids, open(os.path.join(ARTIFACT_DIR, "task_subset.json"), "w"))
727
+ print(f"[swebench] fixed subset of {n_tasks} tasks (seed={seed}) saved.")
728
+
729
+ results = []
730
+ for name, _ in cache_configs():
731
+ quantizers = None if name == "fp16 (baseline)" else fitted[name]
732
+ try:
733
+ rate = run_swebench_subset(model, tok, quantizers,
734
+ meta["full_layers"], task_ids)
735
+ except NotImplementedError as e:
736
+ print(f"[swebench] {name}: STUB -- {e}")
737
+ rate = None
738
+ results.append({"config": name, "resolve_rate": rate})
739
+ print(f"[swebench] {name}: resolve_rate={rate}")
740
+
741
+ # report DELTAS vs fp16 (robust to absolute-score contamination)
742
+ base = next((r["resolve_rate"] for r in results
743
+ if r["config"] == "fp16 (baseline)"), None)
744
+ print("\n[swebench] resolve rate on fixed subset (delta vs fp16):")
745
+ for r in results:
746
+ d = (None if (r["resolve_rate"] is None or base is None)
747
+ else round(r["resolve_rate"] - base, 4))
748
+ print(f" {r['config']:24s} {r['resolve_rate']} (Δ {d})")
749
+ json.dump(results, open(os.path.join(ARTIFACT_DIR, "swebench_results.json"), "w"),
750
+ indent=2)
751
+
752
+
753
+ def main():
754
+ ap = argparse.ArgumentParser()
755
+ ap.add_argument("--stage", required=True,
756
+ choices=["dump", "fit", "cheap", "swebench"])
757
+ ap.add_argument("--n_calib", type=int, default=16)
758
+ ap.add_argument(
759
+ "--calib_source", type=str, default=None,
760
+ choices=["swesmith", "longbench-hotpotqa"],
761
+ help="dump stage: calibration corpus (default: CALIB_SOURCE env or swesmith)")
762
+ ap.add_argument("--n_eval", type=int, default=64)
763
+ ap.add_argument("--n_tasks", type=int, default=50)
764
+ ap.add_argument(
765
+ "--only", type=str, default=None,
766
+ help="fit stage: comma-separated config names to fit/merge (e.g. "
767
+ "sign-1bit,ternary-bitnet). Skips refitting other configs.")
768
+ args = ap.parse_args()
769
+
770
+ if args.stage == "dump":
771
+ stage_dump(n_calib=args.n_calib, calib_source=args.calib_source)
772
+ elif args.stage == "fit":
773
+ only = [s.strip() for s in args.only.split(",")] if args.only else None
774
+ stage_fit(only=only)
775
+ elif args.stage == "cheap":
776
+ stage_cheap(n_eval=args.n_eval)
777
+ elif args.stage == "swebench":
778
+ stage_swebench(n_tasks=args.n_tasks)
779
+
780
+
781
+ if __name__ == "__main__":
782
+ main()
generate.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """AttnVQ: VQQuantizedCache wired into model.generate()."""
2
+ import torch
3
+ from transformers import AutoModelForCausalLM, AutoTokenizer
4
+ from vqkv.compressed_cache import VQQuantizedCache
5
+
6
+
7
+ # load model
8
+ tok = AutoTokenizer.from_pretrained("poolside/Laguna-XS.2", trust_remote_code=True, fix_mistral_regex=True)
9
+ model = AutoModelForCausalLM.from_pretrained("poolside/Laguna-XS.2", torch_dtype=torch.bfloat16, device_map="cuda", trust_remote_code=True).eval()
10
+
11
+ # load codebooks or fit and use your own
12
+ CODEBOOKS_PATH = "artifacts/codebooks.pt"
13
+ codebooks = torch.load(CODEBOOKS_PATH, map_location="cuda", weights_only=False)
14
+
15
+ # build cache
16
+ quantizers, layers = codebooks["fitted"]["productvq-32x256-2b"], blob["meta"]["full_layers"]
17
+ cache = VQQuantizedCache(quantizers, layers) # persists uint8 codebook indices
18
+
19
+ # generate
20
+ ids = tok("Hello", return_tensors="pt").to(model.device)
21
+ out = model.generate(**ids, max_new_tokens=32, past_key_values=cache, use_cache=True)
22
+ print(tok.decode(out[0, ids["input_ids"].shape[1]:], skip_special_tokens=True))
23
+
24
+ # print memory footprint
25
+ print(cache.memory_footprint())
longbench_eval.py ADDED
@@ -0,0 +1,763 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ longbench_eval.py — LongBench v1 evaluation for AttnVQ and baselines.
3
+
4
+ Stages:
5
+ cheap proxy KV metrics on task contexts (fast, default)
6
+ generate end-to-end task scoring via model.generate (slow)
7
+
8
+ Tasks: qasper, 2wikimqa, hotpotqa, passage_retrieval_en, repobench-p
9
+
10
+ Usage:
11
+ python longbench_eval.py --stage cheap --n_eval 50
12
+ python longbench_eval.py --stage generate --tasks hotpotqa qasper
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import collections
19
+ import json
20
+ import os
21
+ import re
22
+ import string
23
+ from difflib import SequenceMatcher
24
+
25
+ import torch
26
+ from tqdm import tqdm
27
+
28
+ from turbo_benchmark import TurboQuantMSE, QJLResidualIP, turbo_configs # noqa: F401 — unpickle
29
+ from vqkv.quantizers import KIVIScalarKV # noqa: F401
30
+ from vqkv.metrics import (key_cosine, cache_mse, inner_product_distortion,
31
+ attention_output, attn_output_cosine, attn_output_error)
32
+
33
+ MODEL_ID = os.environ.get("LAGUNA_ID", "poolside/Laguna-XS.2")
34
+ ARTIFACT_DIR = os.environ.get("VQKV_ARTIFACTS", "./artifacts")
35
+
36
+ ATTN_WIN = 512
37
+ CHEAP_COLS = ("key_cos", "val_cos", "key_mse", "val_mse",
38
+ "attn_cos", "attn_output_error", "ip_rel", "ip_bias")
39
+
40
+ # Scorers (referenced from TASK_CONFIGS)
41
+ def _normalize(s: str) -> str:
42
+ s = s.lower()
43
+ s = s.translate(str.maketrans("", "", string.punctuation))
44
+ return " ".join(s.split())
45
+
46
+
47
+ def qa_f1_score(prediction: str, ground_truths: list[str], **_) -> float:
48
+ """Max token-level F1 over all reference answers (QA tasks)."""
49
+ pred_toks = _normalize(prediction).split()
50
+ best = 0.0
51
+ for ref in ground_truths:
52
+ ref_toks = _normalize(ref).split()
53
+ if not pred_toks or not ref_toks:
54
+ best = max(best, float(pred_toks == ref_toks))
55
+ continue
56
+ common = collections.Counter(pred_toks) & collections.Counter(ref_toks)
57
+ n_common = sum(common.values())
58
+ if n_common == 0:
59
+ continue
60
+ p = n_common / len(pred_toks)
61
+ r = n_common / len(ref_toks)
62
+ best = max(best, 2 * p * r / (p + r))
63
+ return best
64
+
65
+
66
+ def retrieval_score(prediction: str, ground_truths: list[str], **_) -> float:
67
+ """Passage-retrieval accuracy: extract digit from prediction, match gold.
68
+
69
+ Ground truths are strings like 'Paragraph 3'; we extract the number and
70
+ check whether it appears among the first 10 digits in the prediction.
71
+ Mirrors the LongBench retrieval_score implementation.
72
+ """
73
+ gt_ids = set()
74
+ for ref in ground_truths:
75
+ m = re.findall(r"\d+", ref)
76
+ gt_ids.add(m[0] if m else _normalize(ref))
77
+ pred_nums = re.findall(r"\d+", prediction)
78
+ if not pred_nums:
79
+ return 0.0
80
+ right = sum(1 for n in pred_nums[:10] if n in gt_ids)
81
+ return right / len(pred_nums[:10])
82
+
83
+
84
+ def classification_score(prediction: str, ground_truths: list[str],
85
+ all_classes: list[str] | None = None, **_) -> float:
86
+ """Classification accuracy (TREC).
87
+
88
+ If all_classes is provided (from the dataset example), restrict matches
89
+ to valid class labels to avoid spurious substring hits — matches the
90
+ LongBench classification_score behaviour.
91
+ """
92
+ if all_classes:
93
+ matched = [c for c in all_classes if c.lower() in prediction.lower()]
94
+ return float(any(ref.lower() in [m.lower() for m in matched]
95
+ for ref in ground_truths))
96
+ # Fallback: normalised substring match
97
+ pred_norm = _normalize(prediction)
98
+ return float(any(_normalize(ref) in pred_norm for ref in ground_truths))
99
+
100
+
101
+ def edit_similarity_score(prediction: str, ground_truths: list[str], **_) -> float:
102
+ """Character-level edit similarity for code completion (RepoBench-P).
103
+
104
+ Uses difflib.SequenceMatcher.ratio() — equivalent to
105
+ fuzz.ratio() (not partial_ratio) and zero-dep.
106
+ """
107
+ pred = prediction.strip()
108
+ best = 0.0
109
+ for ref in ground_truths:
110
+ ref_s = ref.strip()
111
+ if not pred and not ref_s:
112
+ best = 1.0
113
+ elif pred and ref_s:
114
+ best = max(best, SequenceMatcher(None, pred, ref_s).ratio())
115
+ return best
116
+
117
+
118
+ # ============================================================================
119
+ # Task configs
120
+ # ============================================================================
121
+ # QA tasks: increasing context length (~3.6K → ~18K tokens) directly probes
122
+ # the compounding-error-vs-length axis from RUNBOOK Step 6.
123
+ # Three extra tasks cover retrieval accuracy, classification, and code completion.
124
+ # ============================================================================
125
+ TASK_CONFIGS: dict[str, dict] = {
126
+ "qasper": dict(
127
+ score_fn=qa_f1_score,
128
+ max_new_tokens=20,
129
+ max_len=16384,
130
+ prompt_template=(
131
+ "Answer the question based on the given passages. "
132
+ "Only give me the answer and do not output any other words.\n\n"
133
+ "The following are given passages.\n{context}\n\n"
134
+ "Answer the question based on the given passages. "
135
+ "Only give me the answer and do not output any other words.\n\n"
136
+ "Question: {input}\nAnswer:"
137
+ ),
138
+ ),
139
+ "2wikimqa": dict(
140
+ score_fn=qa_f1_score,
141
+ max_new_tokens=10,
142
+ max_len=16384,
143
+ prompt_template=(
144
+ "Answer the question based on the given passages. "
145
+ "Only give me the answer and do not output any other words.\n\n"
146
+ "The following are given passages.\n{context}\n\n"
147
+ "Answer the question based on the given passages. "
148
+ "Only give me the answer and do not output any other words.\n\n"
149
+ "Question: {input}\nAnswer:"
150
+ ),
151
+ ),
152
+ "hotpotqa": dict(
153
+ score_fn=qa_f1_score,
154
+ max_new_tokens=32,
155
+ max_len=32768,
156
+ prompt_template=(
157
+ "Answer the question based on the given passages. "
158
+ "Only give me the answer and do not output any other words.\n\n"
159
+ "The following are given passages.\n{context}\n\n"
160
+ "Answer the question based on the given passages. "
161
+ "Only give me the answer and do not output any other words.\n\n"
162
+ "Question: {input}\nAnswer:"
163
+ ),
164
+ ),
165
+ "passage_retrieval_en": dict(
166
+ score_fn=retrieval_score,
167
+ max_new_tokens=10,
168
+ max_len=32768,
169
+ prompt_template=(
170
+ "Below is a record of a series of paragraphs, each from different "
171
+ "documents. Tell me which paragraph the given passage is from.\n\n"
172
+ "{context}\n\n"
173
+ "Based on the above paragraphs, which paragraph does the following "
174
+ "passage come from? Only output the paragraph number. "
175
+ "Do not output any other characters.\n\n"
176
+ "{input}\nAnswer:"
177
+ ),
178
+ ),
179
+ "repobench-p": dict(
180
+ score_fn=edit_similarity_score,
181
+ max_new_tokens=64,
182
+ max_len=16384,
183
+ prompt_template=(
184
+ "Please complete the code given below.\n{context}\n{input}\n"
185
+ ),
186
+ ),
187
+ }
188
+
189
+
190
+ # ============================================================================
191
+ # Model loading
192
+ # ============================================================================
193
+ def load_model_and_meta():
194
+ from transformers import AutoModelForCausalLM, AutoTokenizer
195
+ tok = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True, fix_mistral_regex=True)
196
+ model = AutoModelForCausalLM.from_pretrained(
197
+ MODEL_ID, torch_dtype=torch.bfloat16, device_map="cuda",
198
+ trust_remote_code=True)
199
+ model.eval()
200
+ cfg = model.config
201
+ full_layers = [i for i, t in enumerate(cfg.layer_types) if t == "full_attention"]
202
+ meta = {
203
+ "full_layers": full_layers,
204
+ "n_kv_heads": cfg.num_key_value_heads,
205
+ "n_q_heads": cfg.num_attention_heads,
206
+ "head_dim": cfg.head_dim,
207
+ }
208
+ print(f"[meta] full-attention layers ({len(full_layers)}): {full_layers}")
209
+ print(f"[meta] kv_heads={meta['n_kv_heads']} head_dim={meta['head_dim']}")
210
+ return model, tok, meta
211
+
212
+
213
+ # ============================================================================
214
+ # Generic VQCache
215
+ # ============================================================================
216
+ def make_cache_class(per_layer_fns: dict, target_layers: list):
217
+ """Build a DynamicCache subclass that round-trips K/V through the given fns.
218
+
219
+ per_layer_fns[layer_idx] = {
220
+ "k_fn": callable (N, d) -> (N, d),
221
+ "v_fn": callable (N, d) -> (N, d),
222
+ "per_channel": bool, # True for KIVI/Sign/Ternary (key dim=0 reduction)
223
+ }
224
+
225
+ For non-per-channel quantizers, K and V are flattened to (s*h, d) before
226
+ calling k_fn/v_fn. For per-channel, each head's (s, d) block is passed
227
+ separately so the quantizer's token-axis statistics are per-head (not
228
+ cross-head), matching the KIVI / benchmark.py convention.
229
+ """
230
+ from transformers.cache_utils import DynamicCache
231
+ _target = set(target_layers)
232
+
233
+ class VQCache(DynamicCache):
234
+ def update(self, key_states, value_states, layer_idx, cache_kwargs=None):
235
+ if layer_idx in _target and layer_idx in per_layer_fns:
236
+ entry = per_layer_fns[layer_idx]
237
+ k_fn = entry["k_fn"]
238
+ v_fn = entry["v_fn"]
239
+ per_channel = entry.get("per_channel", False)
240
+
241
+ _b, h, s, d = key_states.shape
242
+
243
+ if per_channel:
244
+ # key_states[0]: (h, s, d) -> (s, h, d)
245
+ kk = key_states[0].permute(1, 0, 2).float()
246
+ k_hat = torch.stack([k_fn(kk[:, hh, :]) for hh in range(h)], dim=1)
247
+ vv = value_states[0].permute(1, 0, 2).float()
248
+ v_hat = torch.stack([v_fn(vv[:, hh, :]) for hh in range(h)], dim=1)
249
+ # (s, h, d) -> (h, s, d) -> (1, h, s, d)
250
+ k_hat = k_hat.permute(1, 0, 2).unsqueeze(0)
251
+ v_hat = v_hat.permute(1, 0, 2).unsqueeze(0)
252
+ else:
253
+ # key_states[0]: (h, s, d) -> (s, h, d) -> (s*h, d)
254
+ kf = key_states[0].permute(1, 0, 2).reshape(-1, d).float()
255
+ vf = value_states[0].permute(1, 0, 2).reshape(-1, d).float()
256
+ # (s*h, d) -> (s, h, d) -> (h, s, d) -> (1, h, s, d)
257
+ k_hat = k_fn(kf).reshape(s, h, d).permute(1, 0, 2).unsqueeze(0)
258
+ v_hat = v_fn(vf).reshape(s, h, d).permute(1, 0, 2).unsqueeze(0)
259
+
260
+ key_states = k_hat.to(key_states.dtype).to(key_states.device)
261
+ value_states = v_hat.to(value_states.dtype).to(value_states.device)
262
+
263
+ return super().update(key_states, value_states, layer_idx, cache_kwargs)
264
+
265
+ return VQCache
266
+
267
+
268
+ # ============================================================================
269
+ # Build unified config list from both codebook files
270
+ # ============================================================================
271
+ def build_all_configs(meta: dict, device, only: list[str] | None = None):
272
+ """Return list of (name, bpe, cache_cls_or_None).
273
+
274
+ cache_cls_or_None: class (not instance) to call as cls() each generation,
275
+ or None for the fp16 baseline (DynamicCache allocated internally by model).
276
+ """
277
+ hd = meta["head_dim"]
278
+ layers = meta["full_layers"]
279
+ configs: list[tuple[str, float, type | None]] = []
280
+
281
+ configs.append(("fp16", 16.0, None))
282
+
283
+ # -- Regular codebooks (ProductVQ, RoPESplit, Scalar, KIVI, Sign, Ternary) --
284
+ cb_path = os.path.join(ARTIFACT_DIR, "codebooks.pt")
285
+ if os.path.exists(cb_path):
286
+ blob = torch.load(cb_path, weights_only=False)
287
+ fitted = blob["fitted"]
288
+ for name, per_layer in fitted.items():
289
+ for q in per_layer.values():
290
+ if hasattr(q, "to"):
291
+ q.to(device)
292
+ q0 = next(iter(per_layer.values()))
293
+ bpe = round(q0.bits_per_element(hd), 4)
294
+ per_channel = (isinstance(q0, KIVIScalarKV)
295
+ or getattr(q0, "per_channel_key", False))
296
+ per_layer_fns = {
297
+ i: {"k_fn": per_layer[i].roundtrip_k,
298
+ "v_fn": per_layer[i].roundtrip_v,
299
+ "per_channel": per_channel}
300
+ for i in layers if i in per_layer
301
+ }
302
+ configs.append((name, bpe, make_cache_class(per_layer_fns, layers)))
303
+ else:
304
+ print(f"[warn] {cb_path} not found — skipping ProductVQ / scalar configs")
305
+
306
+ # -- TurboQuant codebooks --
307
+ tc_path = os.path.join(ARTIFACT_DIR, "turbo_codebooks.pt")
308
+ if os.path.exists(tc_path):
309
+ blob = torch.load(tc_path, weights_only=False)
310
+ fitted = blob["fitted"]
311
+ bits_list = blob["bits"]
312
+ cfg_lookup = {n: (b, use_qjl) for n, b, use_qjl in turbo_configs(bits_list)}
313
+ for name, per_layer in fitted.items():
314
+ b, use_qjl = cfg_lookup[name]
315
+ bpe = round(b + (1 if use_qjl else 0) + 16.0 / hd, 4)
316
+ for entry in per_layer.values():
317
+ entry["kq"].to(device)
318
+ entry["vq"].to(device)
319
+ if entry["qjl"] is not None:
320
+ entry["qjl"].to(device)
321
+ per_layer_fns = {
322
+ i: {"k_fn": per_layer[i]["kq"].roundtrip,
323
+ "v_fn": per_layer[i]["vq"].roundtrip,
324
+ "per_channel": False}
325
+ for i in layers if i in per_layer
326
+ }
327
+ configs.append((name, bpe, make_cache_class(per_layer_fns, layers)))
328
+ else:
329
+ print(f"[warn] {tc_path} not found — skipping TurboQuant configs")
330
+
331
+ if only is not None:
332
+ only_set = set(only)
333
+ configs = [(n, b, c) for n, b, c in configs if n in only_set]
334
+ unknown = only_set - {n for n, _, _ in configs}
335
+ if unknown:
336
+ print(f"[warn] unknown --configs names: {sorted(unknown)}")
337
+
338
+ print(f"[build] {len(configs)} configs loaded")
339
+ for n, b, _ in configs:
340
+ print(f" {n:<32} {b:.3f} bpe")
341
+ return configs
342
+
343
+
344
+ # ============================================================================
345
+ # Per-task generation + scoring
346
+ # ============================================================================
347
+ def _load_longbench(task_name: str):
348
+ """Load a LongBench task, compatible with datasets >= 3.0.
349
+
350
+ datasets >= 3.0 dropped custom dataset-script support and raises
351
+ 'Dataset scripts are no longer supported' for THUDM/LongBench.
352
+ We load the underlying JSONL files from the HF Hub directly instead.
353
+ Tries the English-suffixed file first (_e.jsonl), then the plain name.
354
+ """
355
+ from datasets import load_dataset as _ld
356
+ # Candidates in priority order:
357
+ # {task}_e.jsonl – English-suffixed bilingual tasks
358
+ # {task}.jsonl – single-language tasks
359
+ # {task_underscored}.jsonl – hyphenated names (repobench-p → repobench_p)
360
+ slug = task_name.replace("-", "_")
361
+ candidates = [f"{task_name}_e.jsonl", f"{task_name}.jsonl",
362
+ f"{slug}_e.jsonl", f"{slug}.jsonl"]
363
+ # deduplicate while preserving order
364
+ seen: set[str] = set()
365
+ for fname in [c for c in candidates if not (c in seen or seen.add(c))]:
366
+ try:
367
+ return _ld(
368
+ "json",
369
+ data_files=f"hf://datasets/THUDM/LongBench/data/{fname}",
370
+ split="train",
371
+ )
372
+ except Exception:
373
+ continue
374
+ # Fallback for older datasets versions that still support scripts
375
+ return _ld("THUDM/LongBench", name=task_name, split="test")
376
+
377
+
378
+ def prompt_text(task_cfg: dict, example: dict) -> str:
379
+ return task_cfg["prompt_template"].format(
380
+ context=example["context"], input=example["input"])
381
+
382
+
383
+ def load_vqkv_fitted(device):
384
+ path = os.path.join(ARTIFACT_DIR, "codebooks.pt")
385
+ if not os.path.exists(path):
386
+ raise FileNotFoundError(f"{path} not found; run benchmark.py --stage fit")
387
+ blob = torch.load(path, weights_only=False)
388
+ fitted, meta = blob["fitted"], blob["meta"]
389
+ for per_layer in fitted.values():
390
+ for q in per_layer.values():
391
+ if hasattr(q, "to"):
392
+ q.to(device)
393
+ return fitted, meta
394
+
395
+
396
+ def load_turbo_fitted(device):
397
+ path = os.path.join(ARTIFACT_DIR, "turbo_codebooks.pt")
398
+ if not os.path.exists(path):
399
+ print(f"[warn] {path} not found — skipping TurboQuant configs")
400
+ return {}, {}
401
+ blob = torch.load(path, weights_only=False)
402
+ fitted = blob["fitted"]
403
+ cfg_lookup = {n: (b, use_qjl) for n, b, use_qjl in turbo_configs(blob["bits"])}
404
+ for per_layer in fitted.values():
405
+ for entry in per_layer.values():
406
+ entry["kq"].to(device)
407
+ entry["vq"].to(device)
408
+ if entry["qjl"] is not None:
409
+ entry["qjl"].to(device)
410
+ return fitted, cfg_lookup
411
+
412
+
413
+ def _aggregate_cheap_rows(trace_rows: list[dict], bpe_lookup: dict) -> list[dict]:
414
+ agg = collections.defaultdict(lambda: collections.defaultdict(list))
415
+ for r in trace_rows:
416
+ for col in CHEAP_COLS:
417
+ agg[r["config"]][col].append(r[col])
418
+ summary = []
419
+ for name in bpe_lookup:
420
+ if name not in agg:
421
+ continue
422
+ cols = agg[name]
423
+ n = len(cols["key_cos"])
424
+ row = {"config": name, "bits_per_elt": bpe_lookup[name], "n_traces": n}
425
+ for col in CHEAP_COLS:
426
+ row[col] = round(sum(cols[col]) / n, 5)
427
+ summary.append(row)
428
+ return summary
429
+
430
+
431
+ def _print_cheap_table(task_name: str, summary: list[dict]):
432
+ print(f"\n[cheap/{task_name}] mean metrics:")
433
+ print(f" {'config':32s} {'bpe':>5} {'key_cos':>8} {'val_cos':>8} "
434
+ f"{'key_mse':>9} {'val_mse':>9} {'attn_cos':>9} {'attn_err':>9} "
435
+ f"{'ip_rel':>8} {'ip_bias':>9}")
436
+ for row in summary:
437
+ print(f" {row['config']:32s} {row['bits_per_elt']:5.2f} "
438
+ f"{row['key_cos']:8.4f} {row['val_cos']:8.4f} "
439
+ f"{row['key_mse']:9.5f} {row['val_mse']:9.5f} "
440
+ f"{row['attn_cos']:9.4f} {row['attn_output_error']:9.4f} "
441
+ f"{row['ip_rel']:8.5f} {row['ip_bias']:9.6f}")
442
+
443
+
444
+ def run_cheap_task(model, tok, meta, task_name: str, task_cfg: dict,
445
+ vqkv_fitted: dict, turbo_fitted: dict,
446
+ turbo_cfg_lookup: dict, bpe_lookup: dict,
447
+ config_filter: set[str] | None, n_eval: int,
448
+ min_len: int = 2048) -> list[dict]:
449
+ """Dump fp16 caches on n_eval LongBench prompts; score all quantizer configs."""
450
+ from transformers.cache_utils import DynamicCache
451
+
452
+ ds = _load_longbench(task_name)
453
+ subset = ds.select(range(min(n_eval, len(ds))))
454
+ max_len = task_cfg["max_len"]
455
+ full = meta["full_layers"]
456
+ hd = meta["head_dim"]
457
+ n_q = meta.get("n_q_heads", 48)
458
+ dev = model.device
459
+ if str(dev) == "meta":
460
+ dev = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
461
+
462
+ print(f"\n[cheap/{task_name}] {len(subset)} examples, max_len={max_len}")
463
+
464
+ class EvalDump(DynamicCache):
465
+ def __init__(self):
466
+ super().__init__()
467
+ self.d = {i: {} for i in full}
468
+
469
+ def update(self, ks, vs, li, ck=None):
470
+ if li in set(full):
471
+ self.d[li]["k"] = ks.detach()[0].permute(1, 0, 2).float()
472
+ self.d[li]["v"] = vs.detach()[0].permute(1, 0, 2).float()
473
+ return super().update(ks, vs, li, ck)
474
+
475
+ trace_rows: list[dict] = []
476
+ n_used = 0
477
+
478
+ for ex in tqdm(subset, desc=f"cheap/{task_name}"):
479
+ text = prompt_text(task_cfg, ex)
480
+ ids = tok(text, return_tensors="pt", truncation=True,
481
+ max_length=max_len).to(dev)
482
+ if ids["input_ids"].shape[1] < min_len:
483
+ continue
484
+
485
+ cache = EvalDump()
486
+ with torch.no_grad():
487
+ model.model(**ids, past_key_values=cache, use_cache=True)
488
+ n_used += 1
489
+
490
+ synth_q = {}
491
+ for i in full:
492
+ s = cache.d[i]["k"].shape[0]
493
+ win = min(s, ATTN_WIN)
494
+ q_rand = torch.randn(win, n_q, hd, device=dev)
495
+ synth_q[i] = q_rand / q_rand.norm(dim=-1, keepdim=True).clamp_min(1e-8)
496
+
497
+ # -- vqkv configs (ProductVQ, scalar, KIVI, …) --
498
+ for name, per_layer in vqkv_fitted.items():
499
+ if config_filter and name not in config_filter:
500
+ continue
501
+ acc = collections.defaultdict(float)
502
+ nL = 0
503
+ for i in full:
504
+ k = cache.d[i]["k"]
505
+ v = cache.d[i]["v"]
506
+ q = per_layer[i]
507
+ s, h, d = k.shape
508
+ per_channel = (
509
+ isinstance(q, KIVIScalarKV)
510
+ or getattr(q, "per_channel_key", False)
511
+ )
512
+ if per_channel:
513
+ k_hat = torch.stack([q.roundtrip_k(k[:, hh, :]) for hh in range(h)], 1)
514
+ v_hat = torch.stack([q.roundtrip_v(v[:, hh, :]) for hh in range(h)], 1)
515
+ else:
516
+ k_hat = q.roundtrip_k(k.reshape(-1, d)).reshape(s, h, d)
517
+ v_hat = q.roundtrip_v(v.reshape(-1, d)).reshape(s, h, d)
518
+
519
+ acc["key_cos"] += key_cosine(k, k_hat)
520
+ acc["val_cos"] += key_cosine(v, v_hat)
521
+ acc["key_mse"] += cache_mse(k, k_hat)
522
+ acc["val_mse"] += cache_mse(v, v_hat)
523
+
524
+ win = min(s, ATTN_WIN)
525
+ kw, kw_hat = k[-win:], k_hat[-win:]
526
+ vw, vw_hat = v[-win:], v_hat[-win:]
527
+ q_syn = synth_q[i]
528
+
529
+ out_ref, _ = attention_output(q_syn, kw, vw, n_q)
530
+ out_hat, _ = attention_output(q_syn, kw_hat, vw_hat, n_q)
531
+ acc["attn_cos"] += attn_output_cosine(out_ref, out_hat)
532
+ acc["attn_output_error"] += attn_output_error(out_ref, out_hat)
533
+
534
+ ip = inner_product_distortion(q_syn, kw, kw_hat)
535
+ acc["ip_rel"] += ip["ip_rel_err"]
536
+ acc["ip_bias"] += ip["ip_bias"]
537
+ nL += 1
538
+
539
+ trace_rows.append({
540
+ "task": task_name,
541
+ "trace_len": ids["input_ids"].shape[1],
542
+ "config": name,
543
+ **{col: acc[col] / nL for col in CHEAP_COLS},
544
+ })
545
+
546
+ # -- TurboQuant configs --
547
+ for name, per_layer in turbo_fitted.items():
548
+ if config_filter and name not in config_filter:
549
+ continue
550
+ acc = collections.defaultdict(float)
551
+ nL = 0
552
+ for i in full:
553
+ k = cache.d[i]["k"]
554
+ v = cache.d[i]["v"]
555
+ e = per_layer[i]
556
+ s, h, d = k.shape
557
+ k_hat = e["kq"].roundtrip(k.reshape(-1, d)).reshape(s, h, d)
558
+ v_hat = e["vq"].roundtrip(v.reshape(-1, d)).reshape(s, h, d)
559
+
560
+ acc["key_cos"] += key_cosine(k, k_hat)
561
+ acc["val_cos"] += key_cosine(v, v_hat)
562
+ acc["key_mse"] += cache_mse(k, k_hat)
563
+ acc["val_mse"] += cache_mse(v, v_hat)
564
+
565
+ win = min(s, ATTN_WIN)
566
+ kw, kw_hat = k[-win:], k_hat[-win:]
567
+ vw, vw_hat = v[-win:], v_hat[-win:]
568
+ q_syn = synth_q[i]
569
+
570
+ out_ref, _ = attention_output(q_syn, kw, vw, n_q)
571
+ out_hat, _ = attention_output(q_syn, kw_hat, vw_hat, n_q)
572
+ acc["attn_cos"] += attn_output_cosine(out_ref, out_hat)
573
+ acc["attn_output_error"] += attn_output_error(out_ref, out_hat)
574
+
575
+ q0 = q_syn[:, 0, :]
576
+ kr0 = kw[:, 0, :]
577
+ kh0 = kw_hat[:, 0, :]
578
+ qi = torch.randint(0, win, (4096,), device=dev)
579
+ ki = torch.randint(0, win, (4096,), device=dev)
580
+ ip_ref = (q0[qi] * kr0[ki]).sum(-1)
581
+ if e["qjl"] is not None:
582
+ ip_hat = e["qjl"].estimate_ip(q0[qi], kr0[ki])
583
+ else:
584
+ ip_hat = (q0[qi] * kh0[ki]).sum(-1)
585
+ acc["ip_bias"] += (ip_hat - ip_ref).mean().item()
586
+ acc["ip_rel"] += ((ip_hat - ip_ref).abs() /
587
+ ip_ref.abs().clamp_min(1e-6)).mean().item()
588
+ nL += 1
589
+
590
+ trace_rows.append({
591
+ "task": task_name,
592
+ "trace_len": ids["input_ids"].shape[1],
593
+ "config": name,
594
+ **{col: acc[col] / nL for col in CHEAP_COLS},
595
+ })
596
+
597
+ print(f"[cheap/{task_name}] used {n_used}/{len(subset)} traces (min_len={min_len})")
598
+ return trace_rows
599
+
600
+
601
+ def stage_cheap(tasks: list[str], n_eval: int, config_filter: set[str] | None):
602
+ model, tok, model_meta = load_model_and_meta()
603
+ dev = model.device
604
+ if str(dev) == "meta":
605
+ dev = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
606
+
607
+ vqkv_fitted, cb_meta = load_vqkv_fitted(dev)
608
+ turbo_fitted, turbo_cfg_lookup = load_turbo_fitted(dev)
609
+ meta = cb_meta if cb_meta else model_meta
610
+ hd = meta["head_dim"]
611
+
612
+ bpe_lookup: dict[str, float] = {}
613
+ for name, per_layer in vqkv_fitted.items():
614
+ q0 = next(iter(per_layer.values()))
615
+ bpe_lookup[name] = round(q0.bits_per_element(hd), 4)
616
+ for name in turbo_cfg_lookup:
617
+ if name in turbo_fitted:
618
+ b, use_qjl = turbo_cfg_lookup[name]
619
+ bpe_lookup[name] = round(b + (1 if use_qjl else 0) + 16.0 / hd, 4)
620
+
621
+ if config_filter:
622
+ bpe_lookup = {k: v for k, v in bpe_lookup.items() if k in config_filter}
623
+ print(f"[cheap] config filter: {len(bpe_lookup)} configs")
624
+
625
+ all_trace_rows: list[dict] = []
626
+ all_summary: list[dict] = []
627
+
628
+ for task_name in tasks:
629
+ task_cfg = TASK_CONFIGS[task_name]
630
+ # trec few-shot prompts can be shorter than agentic traces
631
+ min_len = 512 if task_name == "trec" else 2048
632
+ rows = run_cheap_task(
633
+ model, tok, meta, task_name, task_cfg,
634
+ vqkv_fitted, turbo_fitted, turbo_cfg_lookup, bpe_lookup,
635
+ config_filter, n_eval, min_len=min_len,
636
+ )
637
+ all_trace_rows.extend(rows)
638
+ summary = _aggregate_cheap_rows(rows, bpe_lookup)
639
+ for row in summary:
640
+ row["task"] = task_name
641
+ all_summary.extend(summary)
642
+ _print_cheap_table(task_name, summary)
643
+
644
+ out_path = os.path.join(ARTIFACT_DIR, "longbench_cheap_metrics.json")
645
+ with open(out_path, "w") as fh:
646
+ json.dump(all_summary, fh, indent=2)
647
+ print(f"\n[cheap] saved -> {out_path} ({len(all_summary)} task×config rows)")
648
+
649
+
650
+ # ============================================================================
651
+ # Per-task generation + scoring (--stage generate)
652
+ # ============================================================================
653
+ def run_task(model, tok, task_name: str, task_cfg: dict,
654
+ configs: list, n_eval: int) -> dict[str, float]:
655
+ """Run every config on n_eval examples. Returns {config_name: mean score}."""
656
+ ds = _load_longbench(task_name)
657
+ subset = ds.select(range(min(n_eval, len(ds))))
658
+ print(f"\n[{task_name}] {len(subset)} examples, "
659
+ f"max_len={task_cfg['max_len']}, max_new={task_cfg['max_new_tokens']}")
660
+
661
+ score_fn = task_cfg.get("score_fn", qa_f1_score)
662
+ max_new = task_cfg["max_new_tokens"]
663
+ max_len = task_cfg["max_len"]
664
+ scores: dict[str, list[float]] = {name: [] for name, _, _ in configs}
665
+
666
+ for ex in tqdm(subset, desc=task_name):
667
+ prompt = prompt_text(task_cfg, ex)
668
+ refs = (ex["answers"] if isinstance(ex["answers"], list)
669
+ else [ex["answers"]])
670
+ # all_classes is present in TREC examples; ignored by other scorers
671
+ all_classes = ex.get("all_classes") or None
672
+ ids = tok(prompt, return_tensors="pt", truncation=True,
673
+ max_length=max_len).to(model.device)
674
+
675
+ for name, _bpe, cache_cls in configs:
676
+ cache = cache_cls() if cache_cls is not None else None
677
+ with torch.no_grad():
678
+ out = model.generate(
679
+ **ids,
680
+ max_new_tokens=max_new,
681
+ do_sample=False,
682
+ past_key_values=cache,
683
+ use_cache=True,
684
+ )
685
+ pred = tok.decode(out[0, ids["input_ids"].shape[1]:],
686
+ skip_special_tokens=True).strip()
687
+ scores[name].append(score_fn(pred, refs, all_classes=all_classes))
688
+
689
+ return {name: sum(vs) / len(vs) for name, vs in scores.items() if vs}
690
+
691
+
692
+ # ============================================================================
693
+ # Main
694
+ # ============================================================================
695
+ def stage_generate(tasks: list[str], n_eval: int, config_filter: list[str] | None):
696
+ model, tok, meta = load_model_and_meta()
697
+ device = model.device
698
+ if str(device) == "meta":
699
+ device = torch.device("cuda:0")
700
+
701
+ configs = build_all_configs(meta, device, only=config_filter)
702
+ print(f"\n[generate] {len(configs)} configs × {len(tasks)} tasks × "
703
+ f"{n_eval} examples each")
704
+
705
+ all_results: dict[str, dict[str, float]] = {}
706
+ for task_name in tasks:
707
+ all_results[task_name] = run_task(
708
+ model, tok, task_name, TASK_CONFIGS[task_name], configs, n_eval)
709
+
710
+ records = []
711
+ print(f"\n{'task':<22} {'config':<32} {'bpe':>5} {'score':>6} {'Δ':>8}")
712
+ print("-" * 78)
713
+ for task_name in tasks:
714
+ task_scores = all_results[task_name]
715
+ fp16_score = task_scores.get("fp16")
716
+ for name, bpe, _ in configs:
717
+ score = task_scores.get(name)
718
+ delta = (None if (score is None or fp16_score is None)
719
+ else round(score - fp16_score, 4))
720
+ delta_str = f"{delta:+.4f}" if delta is not None else " —"
721
+ print(f"{task_name:<22} {name:<32} {bpe:5.2f} "
722
+ f"{score:6.4f} {delta_str}")
723
+ records.append({
724
+ "task": task_name,
725
+ "config": name,
726
+ "bpe": bpe,
727
+ "score": round(score, 4) if score is not None else None,
728
+ "delta": delta,
729
+ })
730
+ print()
731
+
732
+ out_path = os.path.join(ARTIFACT_DIR, "longbench_results.json")
733
+ with open(out_path, "w") as fh:
734
+ json.dump(records, fh, indent=2)
735
+ print(f"[generate] saved -> {out_path}")
736
+
737
+
738
+ def main():
739
+ ap = argparse.ArgumentParser(
740
+ description=__doc__,
741
+ formatter_class=argparse.RawDescriptionHelpFormatter)
742
+ ap.add_argument("--stage", choices=["cheap", "generate"], default="cheap",
743
+ help="cheap: KV proxy metrics (default); generate: task scores")
744
+ ap.add_argument("--tasks", nargs="+", default=list(TASK_CONFIGS),
745
+ choices=list(TASK_CONFIGS),
746
+ help="LongBench tasks to run (default: all 6)")
747
+ ap.add_argument("--n_eval", type=int, default=50,
748
+ help="Examples per task (default: 50)")
749
+ ap.add_argument("--configs", nargs="+", default=None,
750
+ help="Config names to include (default: all). "
751
+ "E.g. --configs productvq-32x256-2b turbo-mse-2b")
752
+ args = ap.parse_args()
753
+
754
+ config_filter = set(args.configs) if args.configs else None
755
+
756
+ if args.stage == "cheap":
757
+ stage_cheap(args.tasks, args.n_eval, config_filter)
758
+ else:
759
+ stage_generate(args.tasks, args.n_eval, args.configs)
760
+
761
+
762
+ if __name__ == "__main__":
763
+ main()
turbo_benchmark.py ADDED
@@ -0,0 +1,531 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ turbo_benchmark.py — faithful TurboQuant baseline (Zandieh et al., ICLR 2026).
3
+
4
+ Haar rotation + per-coordinate Lloyd-Max codebooks + optional QJL residual for
5
+ unbiased inner-product estimation. Separate from benchmark.py; writes
6
+ artifacts/turbo_codebooks.pt.
7
+
8
+ Usage:
9
+ python benchmark.py --stage dump # or use existing calib_caches.pt
10
+ python turbo_benchmark.py --stage fit
11
+ python turbo_benchmark.py --stage cheap
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import os
19
+ import time
20
+
21
+ import torch
22
+
23
+ from vqkv.metrics import (key_cosine, cache_mse, inner_product_distortion,
24
+ attention_output, attn_output_cosine, attn_output_error)
25
+
26
+ ARTIFACT_DIR = os.environ.get("VQKV_ARTIFACTS", "./artifacts")
27
+ MODEL_ID = os.environ.get("LAGUNA_ID", "poolside/Laguna-XS.2")
28
+ CALIB_DATASET = os.environ.get("CALIB_DATASET", "SWE-bench/SWE-smith-trajectories")
29
+ CALIB_SPLIT = os.environ.get("CALIB_SPLIT", "tool")
30
+ EVAL_SOURCE = os.environ.get("EVAL_SOURCE", "swesmith")
31
+
32
+ _HOTPOTQA_PROMPT = (
33
+ "Answer the question based on the given passages. "
34
+ "Only give me the answer and do not output any other words.\n\n"
35
+ "The following are given passages.\n{context}\n\n"
36
+ "Answer the question based on the given passages. "
37
+ "Only give me the answer and do not output any other words.\n\n"
38
+ "Question: {input}\nAnswer:"
39
+ )
40
+
41
+
42
+ # ============================================================================
43
+ # Lloyd-Max 1-D scalar quantizer (the MSE-optimal levels for a given sample)
44
+ # ============================================================================
45
+ def lloyd_max_1d(samples: torch.Tensor, n_levels: int, iters: int = 30,
46
+ seed: int = 0) -> torch.Tensor:
47
+ """Optimal scalar (Lloyd-Max) codebook for a 1-D distribution given samples.
48
+
49
+ samples: (M,) 1-D values drawn from the (concentrated) coordinate dist.
50
+ returns: (n_levels,) sorted reconstruction levels.
51
+
52
+ This is 1-D k-means; for the rotated unit-norm coordinates the distribution
53
+ is the same across coordinates in high dim, so one shared codebook per
54
+ bit-width suffices (matching the paper's single precomputed Beta codebook).
55
+ """
56
+ s = samples.flatten()
57
+ s = s[torch.isfinite(s)]
58
+ # init levels at quantiles so empty bins are rare
59
+ qs = torch.linspace(0.0, 1.0, n_levels + 2, device=s.device)[1:-1]
60
+ levels = torch.quantile(s, qs)
61
+ for _ in range(iters):
62
+ # assign each sample to nearest level
63
+ idx = torch.bucketize(s, (levels[1:] + levels[:-1]) / 2)
64
+ new = levels.clone()
65
+ for j in range(n_levels):
66
+ m = idx == j
67
+ if m.any():
68
+ new[j] = s[m].mean()
69
+ shift = (new - levels).abs().max()
70
+ levels = new
71
+ if shift < 1e-6:
72
+ break
73
+ return torch.sort(levels).values
74
+
75
+
76
+ def _haar_rotation(d: int, seed: int = 0, device=None) -> torch.Tensor:
77
+ g = torch.Generator().manual_seed(seed)
78
+ a = torch.randn(d, d, generator=g)
79
+ q, r = torch.linalg.qr(a)
80
+ # sign-correct so Q is Haar-distributed (QR sign ambiguity)
81
+ q = q * torch.sign(torch.diag(r)).unsqueeze(0)
82
+ if device is not None:
83
+ q = q.to(device)
84
+ return q
85
+
86
+
87
+ # ============================================================================
88
+ # TurboQuant-MSE : rotation -> per-coordinate Lloyd-Max -> norm rescale
89
+ # ============================================================================
90
+ class TurboQuantMSE:
91
+ def __init__(self, nbits: int = 3, seed: int = 0):
92
+ self.nbits = nbits
93
+ self.seed = seed
94
+ self._rot = None # (d, d)
95
+ self._levels = None # (K,) shared Lloyd-Max levels
96
+
97
+ def fit(self, calib: torch.Tensor):
98
+ """calib: (N, d). Fit rotation + shared per-coordinate level set."""
99
+ d = calib.shape[-1]
100
+ self._rot = _haar_rotation(d, self.seed, calib.device)
101
+ xn = calib / calib.norm(dim=-1, keepdim=True).clamp_min(1e-8)
102
+ r = xn @ self._rot # (N, d) rotated unit-norm coords
103
+ # all coordinates share the same concentrated dist -> pool them
104
+ pool = r.flatten()
105
+ if pool.numel() > 2_000_000: # cap for speed
106
+ pool = pool[torch.randperm(pool.numel(), device=pool.device)[:2_000_000]]
107
+ self._levels = lloyd_max_1d(pool, 1 << self.nbits)
108
+ return self
109
+
110
+ def _quantize(self, x):
111
+ norms = x.norm(dim=-1, keepdim=True).clamp_min(1e-8)
112
+ xn = x / norms
113
+ y = xn @ self._rot
114
+ edges = (self._levels[1:] + self._levels[:-1]) / 2
115
+ idx = torch.bucketize(y, edges).clamp(0, self._levels.numel() - 1)
116
+ return idx, norms
117
+
118
+ def roundtrip(self, x):
119
+ idx, norms = self._quantize(x)
120
+ y_hat = self._levels[idx]
121
+ x_hat = y_hat @ self._rot.T
122
+ return x_hat * norms
123
+
124
+ def to(self, device):
125
+ self._rot = self._rot.to(device)
126
+ self._levels = self._levels.to(device)
127
+ return self
128
+
129
+
130
+ # ============================================================================
131
+ # QJL inner-product estimator (faithful, ASYMMETRIC) -- NOT a reconstruction.
132
+ #
133
+ # Important: QJL does not reconstruct a vector. It estimates <q, k> directly,
134
+ # with the query JL-transformed but UNQUANTIZED and only the key residual
135
+ # sign-quantized (the asymmetric estimator of Zandieh et al.). It therefore
136
+ # cannot be expressed as roundtrip(x)->x_hat without reintroducing the very
137
+ # bias it removes. We expose it as a standalone estimator used only in the
138
+ # inner-product-bias evaluation, not in the cosine/MSE reconstruction metrics.
139
+ #
140
+ # <q, k> ~= <q, k_hat_mse> + (sqrt(pi/2) * ||r|| / m) * sum_i (g_i . q) * sign(g_i . r)
141
+ #
142
+ # where r = k_n - k_hat_mse_n is the residual in unit-norm space, g_i are the
143
+ # rows of a gaussian JL matrix, and ||r|| is the stored residual norm.
144
+ # ============================================================================
145
+ class QJLResidualIP:
146
+ def __init__(self, mse: "TurboQuantMSE", qjl_rows: int = None, seed: int = 0):
147
+ self.mse = mse
148
+ self.seed = seed
149
+ self.qjl_rows = qjl_rows
150
+ self._G = None
151
+
152
+ def fit(self, calib: torch.Tensor):
153
+ d = calib.shape[-1]
154
+ m = self.qjl_rows or d
155
+ g = torch.Generator().manual_seed(self.seed + 7)
156
+ self._G = torch.randn(m, d, generator=g).to(calib.device)
157
+ return self
158
+
159
+ def estimate_ip(self, q, k):
160
+ """Unbiased estimate of rowwise <q, k> using MSE stage + QJL residual.
161
+ q, k: (N, d). Returns (N,) inner-product estimates."""
162
+ m = self._G.shape[0]
163
+ # stage-1 reconstruction in unit-norm space
164
+ knorm = k.norm(dim=-1, keepdim=True).clamp_min(1e-8)
165
+ kn = k / knorm
166
+ idx, _ = self.mse._quantize(k)
167
+ kn_hat = (self.mse._levels[idx]) @ self.mse._rot.T
168
+ score1 = (q * (kn_hat * knorm)).sum(-1) # <q, k_hat_mse>
169
+ # stage-2 residual correction (asymmetric: q unquantized, residual signed)
170
+ r = (kn - kn_hat)
171
+ rnorm = r.norm(dim=-1, keepdim=True).clamp_min(1e-12)
172
+ rn = r / rnorm
173
+ qg = q @ self._G.T # (N, m) query JL, unquantized
174
+ rs = torch.sign(rn @ self._G.T) # (N, m) residual sign bits
175
+ score2 = ((torch.pi / 2) ** 0.5 / m) * (qg * rs).sum(-1) * rnorm.squeeze(-1) * knorm.squeeze(-1)
176
+ return score1 + score2
177
+
178
+ def to(self, device):
179
+ self.mse.to(device)
180
+ self._G = self._G.to(device)
181
+ return self
182
+
183
+
184
+ # ============================================================================
185
+ # Config table. Each TurboQuant variant uses TurboQuant-MSE for RECONSTRUCTION
186
+ # (the metric that feeds cosine/MSE). For keys we ALSO build a QJL inner-product
187
+ # estimator (the faithful Prod second stage), reported only in the ip_bias
188
+ # column since QJL is an IP estimator, not a reconstruction.
189
+ #
190
+ # `qjl=True` marks variants that add the QJL key estimator. 1-bit is included
191
+ # as the aggressive floor; at 1 bit the MSE stage is effectively sign, so the
192
+ # QJL residual carries most of the IP fidelity there.
193
+ # ============================================================================
194
+ def turbo_configs(bits_list):
195
+ cfgs = []
196
+ for b in bits_list:
197
+ cfgs.append((f"turbo-mse-{b}b", b, False))
198
+ cfgs.append((f"turbo-prod-{b}b (K:+qjl)", b, True))
199
+ return cfgs
200
+
201
+
202
+ # ============================================================================
203
+ # STAGE: fit -- per-layer codebooks from calib_caches.pt
204
+ # ============================================================================
205
+ def stage_fit(bits_list):
206
+ path = os.path.join(ARTIFACT_DIR, "calib_caches.pt")
207
+ if not os.path.exists(path):
208
+ raise FileNotFoundError(
209
+ f"{path} not found. Run `python benchmark.py --stage dump` first "
210
+ f"to produce the calibration caches this script reuses.")
211
+ blob = torch.load(path, weights_only=False)
212
+ calib, meta = blob["calib"], blob["meta"]
213
+ hd = meta["head_dim"]
214
+ dev = "cuda" if torch.cuda.is_available() else "cpu"
215
+
216
+ fitted = {}
217
+ for name, b, use_qjl in turbo_configs(bits_list):
218
+ per_layer = {}
219
+ t0 = time.time()
220
+ for i, c in calib.items():
221
+ kf = c["k"].reshape(-1, hd)[:200_000].to(dev)
222
+ vf = c["v"].reshape(-1, hd)[:200_000].to(dev)
223
+ kq = TurboQuantMSE(nbits=b).fit(kf)
224
+ vq = TurboQuantMSE(nbits=b).fit(vf)
225
+ qjl = QJLResidualIP(kq).fit(kf) if use_qjl else None
226
+ per_layer[i] = {"kq": kq, "vq": vq, "qjl": qjl}
227
+ fitted[name] = per_layer
228
+ print(f"[fit] {name}: {len(per_layer)} layer-codebooks in {time.time()-t0:.1f}s")
229
+ torch.save({"fitted": fitted, "meta": meta, "bits": bits_list},
230
+ os.path.join(ARTIFACT_DIR, "turbo_codebooks.pt"))
231
+ print(f"[fit] saved -> {ARTIFACT_DIR}/turbo_codebooks.pt")
232
+
233
+
234
+ # ============================================================================
235
+ # Trace flattening -- minimal copy (kept independent of benchmark.py)
236
+ # ============================================================================
237
+ def flatten_trace(example, tok) -> str:
238
+ raw = (example.get("messages") or example.get("trajectory")
239
+ or example.get("conversations"))
240
+ if raw is None:
241
+ return json.dumps(example)[:200_000]
242
+ if isinstance(raw, str):
243
+ try:
244
+ raw = json.loads(raw)
245
+ except json.JSONDecodeError:
246
+ return raw[:200_000]
247
+ norm = []
248
+ for m in raw:
249
+ role = m.get("role") or m.get("from") or "user"
250
+ content = m.get("content") or m.get("value") or ""
251
+ if isinstance(content, list):
252
+ content = "\n".join(
253
+ it.get("text", str(it)) if isinstance(it, dict) else str(it)
254
+ for it in content)
255
+ tc = m.get("tool_calls")
256
+ if tc:
257
+ tct = json.dumps(tc, ensure_ascii=False)
258
+ content = (content + "\n" + tct).strip() if content else tct
259
+ role = {"human": "user", "gpt": "assistant", "tool": "user"}.get(role, role)
260
+ if not content.strip():
261
+ continue
262
+ norm.append({"role": role, "content": content})
263
+ merged = []
264
+ for m in norm:
265
+ if merged and merged[-1]["role"] == m["role"]:
266
+ merged[-1]["content"] += "\n\n" + m["content"]
267
+ else:
268
+ merged.append(dict(m))
269
+ try:
270
+ return tok.apply_chat_template(merged, tokenize=False,
271
+ add_generation_prompt=False)
272
+ except Exception:
273
+ return "\n\n".join(f"{m['role']}: {m['content']}" for m in merged)
274
+
275
+
276
+ def flatten_longbench(example) -> str:
277
+ """Format a LongBench hotpotqa example (context + input) as a plain string."""
278
+ return _HOTPOTQA_PROMPT.format(
279
+ context=example["context"], input=example["input"])
280
+
281
+
282
+ def _load_longbench_hotpotqa():
283
+ """Load THUDM/LongBench hotpotqa, bypassing the deprecated dataset script."""
284
+ from datasets import load_dataset as _ld
285
+ for fname in ("hotpotqa_e.jsonl", "hotpotqa.jsonl"):
286
+ try:
287
+ return _ld(
288
+ "json",
289
+ data_files=f"hf://datasets/THUDM/LongBench/data/{fname}",
290
+ split="train",
291
+ )
292
+ except Exception:
293
+ continue
294
+ return _ld("THUDM/LongBench", name="hotpotqa", split="test")
295
+
296
+
297
+ def _load_cheap_eval_dataset(n_eval: int, eval_source: str, tok):
298
+ """Return (dataset_slice, get_text, max_len, label) for stage_cheap."""
299
+ from datasets import load_dataset
300
+
301
+ if eval_source == "longbench-hotpotqa":
302
+ ds = _load_longbench_hotpotqa()
303
+ # First n_eval rows — disjoint from dump calib (last n_calib rows).
304
+ end = min(n_eval, len(ds))
305
+ ds = ds.select(range(0, end))
306
+ label = f"LongBench hotpotqa (rows 0–{end - 1})"
307
+ return ds, flatten_longbench, 32768, label
308
+
309
+ ds = load_dataset(CALIB_DATASET, split=CALIB_SPLIT)
310
+ start = 500 # held-out offset (matches benchmark.py --stage cheap)
311
+ end = min(start + n_eval, len(ds))
312
+ ds = ds.select(range(start, end))
313
+ label = f"{CALIB_DATASET} split={CALIB_SPLIT} (rows {start}–{end - 1})"
314
+ return ds, lambda ex: flatten_trace(ex, tok), 16384, label
315
+
316
+
317
+ def load_model_and_meta():
318
+ from transformers import AutoModelForCausalLM, AutoTokenizer
319
+ tok = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
320
+ model = AutoModelForCausalLM.from_pretrained(
321
+ MODEL_ID, dtype=torch.bfloat16, device_map="cuda", trust_remote_code=True)
322
+ model.eval()
323
+ cfg = model.config
324
+ full = [i for i, t in enumerate(cfg.layer_types) if t == "full_attention"]
325
+ meta = {"full_layers": full, "n_kv_heads": cfg.num_key_value_heads,
326
+ "n_q_heads": cfg.num_attention_heads,
327
+ "head_dim": cfg.head_dim, "n_layers": cfg.num_hidden_layers}
328
+ print(f"[meta] full-attention layers ({len(full)}): {full}")
329
+ return model, tok, meta
330
+
331
+
332
+ # ============================================================================
333
+ # STAGE: cheap -- full metric set on held-out traces (mirrors benchmark.py)
334
+ # ============================================================================
335
+ def stage_cheap(n_eval=64, max_len=None, eval_source: str | None = None,
336
+ min_len=2048):
337
+ import collections
338
+ from tqdm import tqdm
339
+ from transformers.cache_utils import DynamicCache
340
+
341
+ eval_source = eval_source or EVAL_SOURCE
342
+ blob = torch.load(os.path.join(ARTIFACT_DIR, "turbo_codebooks.pt"),
343
+ weights_only=False)
344
+ fitted, meta = blob["fitted"], blob["meta"]
345
+ full = meta["full_layers"]
346
+ hd = meta["head_dim"]
347
+ n_q = meta.get("n_q_heads", 48) # fallback for turbo_codebooks.pt without this field
348
+
349
+ # Window for O(T²) attention metrics (matches benchmark.py)
350
+ ATTN_WIN = 512
351
+
352
+ # Lookup: config name -> (nbits, use_qjl) for bits_per_elt reporting
353
+ cfg_lookup = {name: (b, use_qjl) for name, b, use_qjl in turbo_configs(blob["bits"])}
354
+
355
+ model, tok, _ = load_model_and_meta()
356
+ dev = model.device
357
+ if str(dev) == "meta":
358
+ dev = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
359
+
360
+ ds, get_text, default_max_len, source_label = _load_cheap_eval_dataset(
361
+ n_eval, eval_source, tok)
362
+ if max_len is None:
363
+ max_len = default_max_len
364
+ print(f"[turbo-cheap] eval_source={eval_source} {source_label} "
365
+ f"rows={len(ds)} max_len={max_len}")
366
+
367
+ for per_layer in fitted.values():
368
+ for entry in per_layer.values():
369
+ entry["kq"].to(dev); entry["vq"].to(dev)
370
+ if entry["qjl"] is not None:
371
+ entry["qjl"].to(dev)
372
+
373
+ class EvalDump(DynamicCache):
374
+ def __init__(self):
375
+ super().__init__(); self.d = {i: {} for i in full}
376
+ def update(self, ks, vs, li, ck=None):
377
+ if li in set(full):
378
+ self.d[li]["k"] = ks.detach()[0].permute(1, 0, 2).float()
379
+ self.d[li]["v"] = vs.detach()[0].permute(1, 0, 2).float()
380
+ return super().update(ks, vs, li, ck)
381
+
382
+ trace_rows = []
383
+ n_used = 0
384
+
385
+ for ex in tqdm(ds, desc=f"turbo-cheap/{eval_source}"):
386
+ text = get_text(ex)
387
+ ids = tok(text, return_tensors="pt", truncation=True,
388
+ max_length=max_len).to(dev)
389
+ if ids["input_ids"].shape[1] < min_len:
390
+ continue
391
+ cache = EvalDump()
392
+ with torch.no_grad():
393
+ model.model(**ids, past_key_values=cache, use_cache=True)
394
+ n_used += 1
395
+
396
+ # Synthetic Q: generated once per (trace, layer), reused across configs.
397
+ # Using synthetic Q for the QJL estimator is more faithful than the
398
+ # previous keys-as-proxy approach: q0[qi] are proper query vectors,
399
+ # kr0[ki] are the keys being estimated.
400
+ synth_q = {}
401
+ for i in full:
402
+ s = cache.d[i]["k"].shape[0]
403
+ win = min(s, ATTN_WIN)
404
+ q_rand = torch.randn(win, n_q, hd, device=dev)
405
+ synth_q[i] = q_rand / q_rand.norm(dim=-1, keepdim=True).clamp_min(1e-8)
406
+
407
+ for name, per_layer in fitted.items():
408
+ acc = collections.defaultdict(float)
409
+ nL = 0
410
+ for i in full:
411
+ k = cache.d[i]["k"]; v = cache.d[i]["v"] # (s, h, d)
412
+ s, h, d = k.shape
413
+ e = per_layer[i]
414
+ k_hat = e["kq"].roundtrip(k.reshape(-1, d)).reshape(s, h, d)
415
+ v_hat = e["vq"].roundtrip(v.reshape(-1, d)).reshape(s, h, d)
416
+
417
+ acc["key_cos"] += key_cosine(k, k_hat)
418
+ acc["val_cos"] += key_cosine(v, v_hat)
419
+ acc["key_mse"] += cache_mse(k, k_hat)
420
+ acc["val_mse"] += cache_mse(v, v_hat)
421
+
422
+ # Windowed attention/IP metrics on last ATTN_WIN tokens
423
+ win = min(s, ATTN_WIN)
424
+ kw, kw_hat = k[-win:], k_hat[-win:]
425
+ vw, vw_hat = v[-win:], v_hat[-win:]
426
+ q_syn = synth_q[i] # (win, n_q, d)
427
+
428
+ out_ref, _ = attention_output(q_syn, kw, vw, n_q)
429
+ out_hat, _ = attention_output(q_syn, kw_hat, vw_hat, n_q)
430
+ acc["attn_cos"] += attn_output_cosine(out_ref, out_hat)
431
+ acc["attn_output_error"] += attn_output_error(out_ref, out_hat)
432
+
433
+ # IP metrics. For Prod configs, use the faithful QJL asymmetric
434
+ # estimator (designed for unbiased <q,k> estimation). For MSE
435
+ # configs, use the plain reconstructed inner product.
436
+ q0 = q_syn[:, 0, :] # (win, d) — head 0 of synthetic Q
437
+ kr0 = kw[:, 0, :] # (win, d) — head 0 of reference K
438
+ kh0 = kw_hat[:, 0, :] # (win, d) — head 0 of reconstructed K
439
+ qi = torch.randint(0, win, (4096,), device=dev)
440
+ ki = torch.randint(0, win, (4096,), device=dev)
441
+ ip_ref = (q0[qi] * kr0[ki]).sum(-1)
442
+ if e["qjl"] is not None:
443
+ ip_hat = e["qjl"].estimate_ip(q0[qi], kr0[ki])
444
+ else:
445
+ ip_hat = (q0[qi] * kh0[ki]).sum(-1)
446
+ acc["ip_bias"] += (ip_hat - ip_ref).mean().item()
447
+ acc["ip_rel"] += ((ip_hat - ip_ref).abs() /
448
+ ip_ref.abs().clamp_min(1e-6)).mean().item()
449
+ nL += 1
450
+
451
+ trace_rows.append({
452
+ "trace_len": ids["input_ids"].shape[1],
453
+ "config": name,
454
+ "key_cos": acc["key_cos"] / nL,
455
+ "val_cos": acc["val_cos"] / nL,
456
+ "key_mse": acc["key_mse"] / nL,
457
+ "val_mse": acc["val_mse"] / nL,
458
+ "attn_cos": acc["attn_cos"] / nL,
459
+ "attn_output_error": acc["attn_output_error"] / nL,
460
+ "ip_rel": acc["ip_rel"] / nL,
461
+ "ip_bias": acc["ip_bias"] / nL,
462
+ })
463
+
464
+ # Aggregate across traces: one summary row per config
465
+ agg = collections.defaultdict(lambda: collections.defaultdict(list))
466
+ for r in trace_rows:
467
+ for col in ("key_cos", "val_cos", "key_mse", "val_mse",
468
+ "attn_cos", "attn_output_error", "ip_rel", "ip_bias"):
469
+ agg[r["config"]][col].append(r[col])
470
+
471
+ COLS = ("key_cos", "val_cos", "key_mse", "val_mse",
472
+ "attn_cos", "attn_output_error", "ip_rel", "ip_bias")
473
+
474
+ summary = []
475
+ for name in cfg_lookup:
476
+ if name not in agg:
477
+ continue
478
+ b, use_qjl = cfg_lookup[name]
479
+ # bpe: nbits/coord + 1 bit/coord for QJL signs (m=d rows) + fp16 norm
480
+ bpe = b + (1 if use_qjl else 0) + 16.0 / hd
481
+ cols = agg[name]
482
+ n = len(cols["key_cos"])
483
+ row = {"config": name, "bits_per_elt": round(bpe, 4),
484
+ "n_traces": n, "eval_source": eval_source}
485
+ for col in COLS:
486
+ row[col] = round(sum(cols[col]) / n, 5)
487
+ summary.append(row)
488
+
489
+ print(f"\n[turbo-cheap] mean metrics over {n_used} traces ({eval_source}):")
490
+ print(f" {'config':30s} {'bpe':>5} {'key_cos':>8} {'val_cos':>8} "
491
+ f"{'key_mse':>9} {'val_mse':>9} {'attn_cos':>9} {'attn_err':>9} "
492
+ f"{'ip_rel':>8} {'ip_bias':>9}")
493
+ for row in summary:
494
+ print(f" {row['config']:30s} {row['bits_per_elt']:5.2f} "
495
+ f"{row['key_cos']:8.4f} {row['val_cos']:8.4f} "
496
+ f"{row['key_mse']:9.5f} {row['val_mse']:9.5f} "
497
+ f"{row['attn_cos']:9.4f} {row['attn_output_error']:9.4f} "
498
+ f"{row['ip_rel']:8.5f} {row['ip_bias']:9.6f}")
499
+
500
+ out_name = ("turbo_cheap_metrics_hotpotqa.json"
501
+ if eval_source == "longbench-hotpotqa"
502
+ else "turbo_cheap_metrics.json")
503
+ out_path = os.path.join(ARTIFACT_DIR, out_name)
504
+ json.dump(summary, open(out_path, "w"), indent=2)
505
+ print(f"[turbo-cheap] saved -> {out_path}")
506
+
507
+
508
+ def main():
509
+ ap = argparse.ArgumentParser(description=__doc__,
510
+ formatter_class=argparse.RawDescriptionHelpFormatter)
511
+ ap.add_argument("--stage", required=True, choices=["fit", "cheap"])
512
+ ap.add_argument("--bits", type=int, nargs="+", default=[4, 2, 1],
513
+ help="bit-widths to sweep (default: 4 2 1)")
514
+ ap.add_argument("--n_eval", type=int, default=64)
515
+ ap.add_argument(
516
+ "--eval_source", type=str, default=None,
517
+ choices=["swesmith", "longbench-hotpotqa"],
518
+ help="cheap stage: eval corpus (default: EVAL_SOURCE env or swesmith)")
519
+ ap.add_argument("--max_len", type=int, default=None,
520
+ help="cheap stage: max prompt tokens (default: 16384 swesmith, "
521
+ "32768 longbench-hotpotqa)")
522
+ args = ap.parse_args()
523
+ if args.stage == "fit":
524
+ stage_fit(args.bits)
525
+ elif args.stage == "cheap":
526
+ stage_cheap(n_eval=args.n_eval, max_len=args.max_len,
527
+ eval_source=args.eval_source)
528
+
529
+
530
+ if __name__ == "__main__":
531
+ main()
vqkv/__init__.py ADDED
File without changes
vqkv/compressed_cache.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ vqkv.compressed_cache — inference-ready KV cache with real memory savings.
3
+
4
+ VQQuantizedCache persists uint8 codebook indices for target layers (not bf16
5
+ reconstructions). Dequantizes transiently per layer on each attention read.
6
+ Without a fused kernel this saves memory but not wall-clock; see memory_footprint().
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+
13
+ import torch
14
+
15
+ try:
16
+ from vqkv.quantizers import ProductVQKV, ScalarKV, KIVIScalarKV
17
+ except ModuleNotFoundError: # flat layout (files beside this one)
18
+ from quantizers import ProductVQKV, ScalarKV, KIVIScalarKV
19
+
20
+
21
+ # ----------------------------------------------------------------------------
22
+ # encode / decode for a *fitted* ProductVQKV (no change to quantizers.py)
23
+ # ----------------------------------------------------------------------------
24
+ # These reach into the already-built batched stacks (cb, mu, sd) that
25
+ # ProductVQKV._ensure_stacked() prepares, so encode(x) then decode(idx) is
26
+ # bit-identical to the existing q.roundtrip_k(x). Promote them to methods on
27
+ # ProductVQKV if you prefer; kept as free functions to leave your file untouched.
28
+
29
+ def _stacked(q: ProductVQKV, which: str):
30
+ q._ensure_stacked()
31
+ return q._k_stacked if which == "k" else q._v_stacked
32
+
33
+
34
+ def pvq_encode(q: ProductVQKV, x: torch.Tensor, which: str = "k") -> torch.Tensor:
35
+ """x: (N, head_dim) -> idx: (N, n_sub) uint8 (nearest codeword per sub-block)."""
36
+ cb, mu, sd = _stacked(q, which) # cb: (n_sub, K, sub_dim)
37
+ n_sub, K, sub_dim = cb.shape
38
+ if K > 256:
39
+ raise ValueError(f"n_codes={K} > 256 needs >1 byte/index; this path is uint8. "
40
+ f"Use K<=256 (all headline configs do) or add bit-packing.")
41
+ N = x.shape[0]
42
+ c_sq = (cb * cb).sum(-1).unsqueeze(1) # (n_sub, 1, K)
43
+ mu_b = mu.permute(1, 0, 2) if mu is not None else None
44
+ sd_b = sd.permute(1, 0, 2) if sd is not None else None
45
+ chunk = max(1, (256 * 1024 * 1024) // (n_sub * K * 4))
46
+ out = []
47
+ for s0 in range(0, N, chunk):
48
+ xc = x[s0:s0 + chunk]
49
+ c = xc.shape[0]
50
+ xb = xc.reshape(c, n_sub, sub_dim).permute(1, 0, 2).contiguous()
51
+ if mu_b is not None:
52
+ xb = (xb - mu_b) / sd_b
53
+ x_sq = (xb * xb).sum(-1, keepdim=True) # (n_sub, c, 1)
54
+ cross = torch.bmm(xb, cb.transpose(1, 2)) # (n_sub, c, K)
55
+ idx = (x_sq - 2 * cross + c_sq).argmin(-1) # (n_sub, c)
56
+ out.append(idx.to(torch.uint8))
57
+ return torch.cat(out, dim=1).T.contiguous() # (N, n_sub) uint8
58
+
59
+
60
+ def pvq_decode(q: ProductVQKV, idx: torch.Tensor, which: str = "k") -> torch.Tensor:
61
+ """idx: (N, n_sub) uint8 -> x_hat: (N, head_dim) (codebook dtype, e.g. fp32)."""
62
+ cb, mu, sd = _stacked(q, which)
63
+ n_sub, K, sub_dim = cb.shape
64
+ mu_b = mu.permute(1, 0, 2) if mu is not None else None
65
+ sd_b = sd.permute(1, 0, 2) if sd is not None else None
66
+ idxT = idx.T.long() # (n_sub, N)
67
+ rec = torch.gather(cb, 1, idxT.unsqueeze(-1).expand(-1, -1, sub_dim)) # (n_sub,N,sub_dim)
68
+ if mu_b is not None:
69
+ rec = rec * sd_b + mu_b
70
+ N = idxT.shape[1]
71
+ return rec.permute(1, 0, 2).reshape(N, n_sub * sub_dim)
72
+
73
+
74
+ def pvq_codebook_bytes(q: ProductVQKV) -> int:
75
+ """Fixed per-layer codebook overhead (bytes), amortized over all tokens."""
76
+ cb_k, _, _ = _stacked(q, "k")
77
+ cb_v, _, _ = _stacked(q, "v")
78
+ return cb_k.numel() * cb_k.element_size() + cb_v.numel() * cb_v.element_size()
79
+
80
+
81
+ # ----------------------------------------------------------------------------
82
+ # Inference-ready cache: compressed store for target layers, native for rest
83
+ # ----------------------------------------------------------------------------
84
+ try:
85
+ from transformers.cache_utils import DynamicCache
86
+ _HAVE_TF = True
87
+ except Exception: # let the file import for the standalone memory harness
88
+ DynamicCache = object
89
+ _HAVE_TF = False
90
+
91
+
92
+ class VQQuantizedCache(DynamicCache):
93
+ """Drop-in cache that persists ProductVQ indices for ``target_layers`` and
94
+ leaves all other layers in native precision (Laguna's sliding-window layers
95
+ are bounded at 512 tokens and don't dominate, so we don't touch them).
96
+
97
+ Memory model: ``key_cache``/``value_cache`` for target layers are never
98
+ populated with full tensors. We keep ``k_codes[layer]`` / ``v_codes[layer]``
99
+ as (seq, n_kv_heads, n_sub) uint8 and dequantize the whole buffer transiently
100
+ each time the layer runs attention.
101
+
102
+ NOTE ON transformers VERSIONS: signatures around DynamicCache shift between
103
+ releases. This targets the modern
104
+ ``update(key_states, value_states, layer_idx, cache_kwargs=None) -> (k, v)``
105
+ interface and overrides ``get_seq_length``. If your pinned version calls
106
+ additional hooks (``reorder_cache`` for beam search, ``crop`` for assisted
107
+ decoding), forward them to the code buffers the same way ``update`` does.
108
+ """
109
+
110
+ def __init__(self, per_layer_quantizers: dict, target_layers, *a, **k):
111
+ super().__init__(*a, **k)
112
+ self.q = per_layer_quantizers
113
+ self.target = set(int(i) for i in target_layers)
114
+ self.k_codes: dict[int, torch.Tensor] = {}
115
+ self.v_codes: dict[int, torch.Tensor] = {}
116
+ self._dtype = None
117
+ self._device = None
118
+
119
+ # -- the hot path --------------------------------------------------------
120
+ def update(self, key_states, value_states, layer_idx, cache_kwargs=None):
121
+ if layer_idx not in self.target or layer_idx not in self.q:
122
+ # native path for sliding-window / non-targeted layers
123
+ return super().update(key_states, value_states, layer_idx, cache_kwargs)
124
+
125
+ self._dtype = key_states.dtype
126
+ self._device = key_states.device
127
+ q = self.q[layer_idx]
128
+ b, h, s, d = key_states.shape # b == 1 in this harness
129
+
130
+ # encode the NEW tokens (token-major, head-minor rows -> (s, h, n_sub))
131
+ kf = key_states[0].transpose(0, 1).reshape(-1, d).float()
132
+ vf = value_states[0].transpose(0, 1).reshape(-1, d).float()
133
+ kc = pvq_encode(q, kf, "k").reshape(s, h, -1)
134
+ vc = pvq_encode(q, vf, "v").reshape(s, h, -1)
135
+
136
+ # append to the persistent compressed buffer
137
+ if layer_idx in self.k_codes:
138
+ self.k_codes[layer_idx] = torch.cat([self.k_codes[layer_idx], kc], dim=0)
139
+ self.v_codes[layer_idx] = torch.cat([self.v_codes[layer_idx], vc], dim=0)
140
+ else:
141
+ self.k_codes[layer_idx] = kc
142
+ self.v_codes[layer_idx] = vc
143
+
144
+ # transiently dequantize the FULL buffer for this layer's attention
145
+ allk, allv = self.k_codes[layer_idx], self.v_codes[layer_idx]
146
+ S = allk.shape[0]
147
+ kfull = pvq_decode(q, allk.reshape(-1, allk.shape[-1]), "k").reshape(S, h, d)
148
+ vfull = pvq_decode(q, allv.reshape(-1, allv.shape[-1]), "v").reshape(S, h, d)
149
+ kfull = kfull.permute(1, 0, 2)[None].to(self._dtype).to(self._device)
150
+ vfull = vfull.permute(1, 0, 2)[None].to(self._dtype).to(self._device)
151
+ return kfull, vfull
152
+
153
+ def get_seq_length(self, layer_idx: int = 0) -> int:
154
+ for li in sorted(self.target):
155
+ if li in self.k_codes:
156
+ return self.k_codes[li].shape[0]
157
+ return super().get_seq_length(layer_idx) if _HAVE_TF else 0
158
+
159
+ # -- live memory readout for the demo ------------------------------------
160
+ def memory_footprint(self) -> dict:
161
+ """Persistent bytes actually held on device, split by source."""
162
+ code_bytes = sum(t.numel() for t in self.k_codes.values()) \
163
+ + sum(t.numel() for t in self.v_codes.values()) # uint8 = 1 B
164
+ cb_bytes = sum(pvq_codebook_bytes(self.q[li]) for li in self.k_codes)
165
+ native_bytes = 0
166
+ if _HAVE_TF:
167
+ for kc in getattr(self, "key_cache", []):
168
+ if isinstance(kc, torch.Tensor):
169
+ native_bytes += kc.numel() * kc.element_size()
170
+ for vc in getattr(self, "value_cache", []):
171
+ if isinstance(vc, torch.Tensor):
172
+ native_bytes += vc.numel() * vc.element_size()
173
+ return {"compressed_indices_B": code_bytes,
174
+ "codebooks_B": cb_bytes,
175
+ "native_layers_B": native_bytes,
176
+ "total_B": code_bytes + cb_bytes + native_bytes}
177
+
178
+
179
+ # ----------------------------------------------------------------------------
180
+ # Honest byte-accounting (model-free) -- drives the memory demo & the harness
181
+ # ----------------------------------------------------------------------------
182
+ class LagunaGeom:
183
+ """Laguna-XS.2 cache geometry (from the config / proposal)."""
184
+ n_layers = 40
185
+ full_layers = 10 # full_attention layers that hold the growing cache
186
+ sliding_layers = 30
187
+ sliding_window = 512
188
+ n_kv_heads = 8
189
+ head_dim = 128
190
+
191
+
192
+ def kv_cache_bytes(context_len: int, bits_per_elt_full: float,
193
+ geom: LagunaGeom = LagunaGeom(),
194
+ bits_per_elt_sliding: float = 16.0) -> dict:
195
+ """Total KV-cache bytes at a context length.
196
+
197
+ Only the full-attention layers carry the growing cache; sliding layers are
198
+ capped at ``sliding_window`` tokens. ``bits_per_elt_full`` is the rate the
199
+ quantizer reports for the compressed layers (use 16.0 for the fp16 baseline).
200
+ The K and V tensors are both counted.
201
+ """
202
+ elts_per_token = geom.n_kv_heads * geom.head_dim * 2 # K and V
203
+ full_tokens = context_len * geom.full_layers
204
+ slide_tokens = min(context_len, geom.sliding_window) * geom.sliding_layers
205
+ full_B = full_tokens * elts_per_token * bits_per_elt_full / 8
206
+ slide_B = slide_tokens * elts_per_token * bits_per_elt_sliding / 8
207
+ return {"full_B": full_B, "sliding_B": slide_B, "total_B": full_B + slide_B}
208
+
209
+
210
+ def compression_vs_fp16(context_len: int, bits_per_elt_full: float,
211
+ geom: LagunaGeom = LagunaGeom()) -> float:
212
+ base = kv_cache_bytes(context_len, 16.0, geom)["total_B"]
213
+ comp = kv_cache_bytes(context_len, bits_per_elt_full, geom)["total_B"]
214
+ return base / comp
215
+
216
+
217
+ # ----------------------------------------------------------------------------
218
+ # self-test: verify encode/decode == roundtrip and show the real byte win
219
+ # ----------------------------------------------------------------------------
220
+ if __name__ == "__main__":
221
+ torch.manual_seed(0)
222
+ N, hd = 4096, 128
223
+ k = torch.randn(N, hd)
224
+ v = torch.randn(N, hd)
225
+
226
+ q = ProductVQKV(n_sub=32, n_codes=256, iters=10).fit(k, v) # 2 bits/elt
227
+ idx = pvq_encode(q, k, "k")
228
+ k_hat = pvq_decode(q, idx, "k")
229
+ rt = q.roundtrip_k(k)
230
+
231
+ print(f"idx dtype/shape : {idx.dtype} {tuple(idx.shape)}")
232
+ print(f"encode->decode == roundtrip : "
233
+ f"{torch.allclose(k_hat, rt, atol=1e-4)} "
234
+ f"(max abs diff {(k_hat - rt).abs().max():.2e})")
235
+
236
+ raw_B = k.numel() * 2 # fp16
237
+ comp_B = idx.numel() * 1 # uint8 indices
238
+ print(f"stored bytes fp16={raw_B} vq-2b={comp_B} ratio={raw_B / comp_B:.1f}x")
239
+ print(f"reported bits/elt: {q.bits_per_element(hd):.3f}")
240
+
241
+ for L in (4096, 32768, 131072):
242
+ r = compression_vs_fp16(L, q.bits_per_element(hd))
243
+ gb = kv_cache_bytes(L, q.bits_per_element(hd))["total_B"] / 1e9
244
+ base = kv_cache_bytes(L, 16.0)["total_B"] / 1e9
245
+ print(f"context {L:>7}: fp16={base:6.2f} GB vq-2b={gb:6.2f} GB ({r:.1f}x)")
vqkv/metrics.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ vqkv.metrics — attention-aware distortion metrics for KV-cache quantization.
3
+
4
+ Reports key cosine, inner-product bias, and attention-output error (not cache
5
+ MSE alone). attention_output() matches Laguna GQA geometry (8 KV / 48 Q heads).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import torch
11
+ import torch.nn.functional as F
12
+
13
+
14
+ def attention_output(q, k, v, n_q_heads, gate=None, scaling=None):
15
+ """Single forward of GQA attention for one layer.
16
+
17
+ q: (T, n_q_heads, head_dim)
18
+ k: (T, n_kv_heads, head_dim)
19
+ v: (T, n_kv_heads, head_dim)
20
+ Returns: (T, n_q_heads, head_dim) attention output (pre o_proj).
21
+ """
22
+ T, n_kv_heads, head_dim = k.shape
23
+ groups = n_q_heads // n_kv_heads
24
+ if scaling is None:
25
+ scaling = head_dim ** -0.5
26
+
27
+ # expand kv heads to q heads (GQA)
28
+ k_exp = k.repeat_interleave(groups, dim=1) # (T, n_q_heads, d)
29
+ v_exp = v.repeat_interleave(groups, dim=1)
30
+
31
+ # (n_q_heads, T, d)
32
+ qh = q.transpose(0, 1)
33
+ kh = k_exp.transpose(0, 1)
34
+ vh = v_exp.transpose(0, 1)
35
+
36
+ scores = torch.matmul(qh, kh.transpose(-1, -2)) * scaling # (H, T, T)
37
+ causal = torch.tril(torch.ones(T, T, dtype=torch.bool, device=q.device))
38
+ scores = scores.masked_fill(~causal, float("-inf"))
39
+ attn = F.softmax(scores, dim=-1)
40
+ out = torch.matmul(attn, vh) # (H, T, d)
41
+ out = out.transpose(0, 1) # (T, H, d)
42
+
43
+ if gate is not None: # Laguna per-head softplus gate
44
+ out = out * F.softplus(gate).unsqueeze(-1)
45
+ return out, attn
46
+
47
+
48
+ def cache_mse(x, x_hat):
49
+ return F.mse_loss(x_hat, x).item()
50
+
51
+
52
+ def attn_output_error(out_ref, out_hat):
53
+ """Relative L2 error in attention output (magnitude + direction combined)."""
54
+ num = (out_hat - out_ref).norm()
55
+ den = out_ref.norm().clamp_min(1e-8)
56
+ return (num / den).item()
57
+
58
+
59
+ def attn_output_cosine(out_ref, out_hat, per_head=True):
60
+ """Cosine similarity between reference and quantized attention outputs.
61
+
62
+ This isolates DIRECTION, which is what propagates through o_proj into the
63
+ residual stream. A quantizer can preserve direction while distorting
64
+ magnitude (or vice versa); relative-L2 conflates the two. On Laguna the
65
+ per-head softplus gate (g_proj) rescales each head AFTER attention, so a
66
+ magnitude error is partly re-absorbed by the gate -- which makes the
67
+ directional (cosine) error the more faithful quality signal here.
68
+
69
+ out_*: (T, H, d). Returns mean cosine over (valid token, head) pairs.
70
+ If per_head=False, flattens heads and measures the full per-token vector.
71
+ """
72
+ if per_head:
73
+ a = out_ref.reshape(-1, out_ref.shape[-1])
74
+ b = out_hat.reshape(-1, out_hat.shape[-1])
75
+ else:
76
+ a = out_ref.reshape(out_ref.shape[0], -1)
77
+ b = out_hat.reshape(out_hat.shape[0], -1)
78
+ cos = torch.nn.functional.cosine_similarity(a, b, dim=-1, eps=1e-8)
79
+ return cos.mean().item()
80
+
81
+
82
+ def magnitude_direction_split(out_ref, out_hat):
83
+ """Decompose attention-output error into a directional part (1 - cosine)
84
+ and a magnitude part (relative norm error per vector). Reporting both shows
85
+ WHICH kind of error a quantizer makes -- the discriminating diagnostic when
86
+ comparing a rotation-based scalar method (TurboQuant) against product VQ.
87
+ """
88
+ a = out_ref.reshape(-1, out_ref.shape[-1])
89
+ b = out_hat.reshape(-1, out_hat.shape[-1])
90
+ cos = torch.nn.functional.cosine_similarity(a, b, dim=-1, eps=1e-8)
91
+ na, nb = a.norm(dim=-1).clamp_min(1e-8), b.norm(dim=-1)
92
+ mag_rel = ((nb - na).abs() / na).mean().item()
93
+ return {"dir_err (1-cos)": round((1 - cos.mean()).item(), 5),
94
+ "mag_rel_err": round(mag_rel, 5)}
95
+
96
+
97
+ def attn_kl(attn_ref, attn_hat):
98
+ """Mean KL(attn_ref || attn_hat) over query positions and heads."""
99
+ a = attn_ref.clamp_min(1e-9)
100
+ b = attn_hat.clamp_min(1e-9)
101
+ kl = (a * (a.log() - b.log())).sum(dim=-1) # (H, T)
102
+ # only count valid (non-masked) rows: those with > 0 mass already normalized
103
+ return kl.mean().item()
104
+
105
+
106
+ def key_cosine(k_ref, k_hat):
107
+ """Mean cosine similarity between original and reconstructed KEY vectors.
108
+
109
+ The attention logit is q.k, which depends on the DIRECTION of k. Key cosine
110
+ is a cheap, query-free proxy for logit fidelity: if directions are preserved,
111
+ logits are preserved up to per-key magnitude. k_*: (T, H, d)."""
112
+ a = k_ref.reshape(-1, k_ref.shape[-1])
113
+ b = k_hat.reshape(-1, k_ref.shape[-1])
114
+ return torch.nn.functional.cosine_similarity(a, b, dim=-1, eps=1e-8).mean().item()
115
+
116
+
117
+ def inner_product_distortion(q, k_ref, k_hat, n_pairs=4096):
118
+ """Relative error in the q.k inner products that drive attention logits.
119
+
120
+ This is the objective TurboQuant (ICLR 2026) argues for over cache MSE: it
121
+ shows MSE-optimal quantizers are BIASED for inner-product estimation. We
122
+ measure |<q,k_hat> - <q,k_ref>| / |<q,k_ref>| over sampled query-key pairs.
123
+
124
+ Also returns the signed mean (bias): a method can be unbiased (mean ~0) yet
125
+ high-variance, or biased -- the distinction TurboQuant's QJL stage targets.
126
+ q: (T, Hq, d); k_*: (T, Hk, d). Uses head 0 of each for a cheap estimate.
127
+ """
128
+ qd = q[:, 0, :]
129
+ kr = k_ref[:, 0, :]
130
+ kh = k_hat[:, 0, :]
131
+ T = qd.shape[0]
132
+ qi = torch.randint(0, T, (n_pairs,), device=qd.device)
133
+ ki = torch.randint(0, T, (n_pairs,), device=qd.device)
134
+ ip_ref = (qd[qi] * kr[ki]).sum(-1)
135
+ ip_hat = (qd[qi] * kh[ki]).sum(-1)
136
+ rel = ((ip_hat - ip_ref).abs() / ip_ref.abs().clamp_min(1e-6)).mean().item()
137
+ bias = (ip_hat - ip_ref).mean().item()
138
+ return {"ip_rel_err": round(rel, 5), "ip_bias": round(bias, 6)}
vqkv/quantizers.py ADDED
@@ -0,0 +1,618 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ vqkv.quantizers — KV-cache quantizers for AttnVQ.
3
+
4
+ ScalarKV, KIVIScalarKV, ProductVQKV (LBG product VQ), RoPESplitVQKV,
5
+ SignScalarKV, TernaryScalarKV. Each exposes fit() + roundtrip_k/v() and
6
+ bits_per_element(). Distortion is evaluated via vqkv.metrics (attention-output
7
+ error, not cache MSE alone).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import math
13
+ from dataclasses import dataclass, field
14
+
15
+ import torch
16
+
17
+
18
+ # ----------------------------------------------------------------------------
19
+ # Utilities
20
+ # ----------------------------------------------------------------------------
21
+ def _affine_quantize(x: torch.Tensor, nbits: int, dim: int):
22
+ """Symmetric-range affine quantization along `dim`. Returns (q, scale, zero)."""
23
+ qmax = (1 << nbits) - 1
24
+ xmin = x.amin(dim=dim, keepdim=True)
25
+ xmax = x.amax(dim=dim, keepdim=True)
26
+ scale = (xmax - xmin).clamp_min(1e-8) / qmax
27
+ zero = torch.round(-xmin / scale)
28
+ q = torch.clamp(torch.round(x / scale) + zero, 0, qmax)
29
+ return q, scale, zero
30
+
31
+
32
+ def _affine_dequantize(q, scale, zero):
33
+ return (q - zero) * scale
34
+
35
+
36
+ # ----------------------------------------------------------------------------
37
+ # Scalar baselines
38
+ # ----------------------------------------------------------------------------
39
+ @dataclass
40
+ class ScalarKV:
41
+ """Per-token affine quantization of both K and V (the transformers default)."""
42
+ nbits: int = 4
43
+
44
+ def fit(self, k_calib, v_calib): # scalar quant is tuning-free
45
+ return self
46
+
47
+ def roundtrip_k(self, k):
48
+ # k: (..., head_dim) -> quantize along head_dim (per-token, per-head)
49
+ q, s, z = _affine_quantize(k, self.nbits, dim=-1)
50
+ return _affine_dequantize(q, s, z)
51
+
52
+ def roundtrip_v(self, v):
53
+ q, s, z = _affine_quantize(v, self.nbits, dim=-1)
54
+ return _affine_dequantize(q, s, z)
55
+
56
+ def bits_per_element(self, head_dim):
57
+ # nbits per element + scale/zero (fp16 each) amortized over head_dim
58
+ return self.nbits + (2 * 16) / head_dim
59
+
60
+
61
+ @dataclass
62
+ class KIVIScalarKV:
63
+ """KIVI-style: keys quantized per-channel, values per-token.
64
+
65
+ Keys are quantized along the TOKEN axis (per channel); values along the
66
+ head_dim axis (per token). Requires a token-axis view, so fit/roundtrip
67
+ operate on a full (N_tokens, head_dim) block per head.
68
+ """
69
+ nbits: int = 4
70
+
71
+ def fit(self, k_calib, v_calib):
72
+ return self
73
+
74
+ def roundtrip_k(self, k):
75
+ # k: (N_tokens, head_dim). per-channel == quantize along token axis (dim=0)
76
+ q, s, z = _affine_quantize(k, self.nbits, dim=0)
77
+ return _affine_dequantize(q, s, z)
78
+
79
+ def roundtrip_v(self, v):
80
+ q, s, z = _affine_quantize(v, self.nbits, dim=-1)
81
+ return _affine_dequantize(q, s, z)
82
+
83
+ def bits_per_element(self, head_dim):
84
+ return self.nbits + (2 * 16) / head_dim
85
+
86
+
87
+ # ----------------------------------------------------------------------------
88
+ # LBG / k-means codebook (the 1980 algorithm, the engine of the project)
89
+ # ----------------------------------------------------------------------------
90
+ def lbg_codebook(data: torch.Tensor, n_codes: int, iters: int = 25,
91
+ seed: int = 0) -> torch.Tensor:
92
+ """Linde-Buzo-Gray vector quantizer design (== Lloyd / k-means for MSE).
93
+
94
+ data: (N, d) training vectors
95
+ returns: (n_codes, d) codebook
96
+ """
97
+ g = torch.Generator().manual_seed(seed)
98
+ N, d = data.shape
99
+ dev = data.device
100
+ idx = torch.randperm(N, generator=g)[:n_codes].to(dev)
101
+ cb = data[idx].clone()
102
+ ones = torch.ones(N, dtype=data.dtype, device=dev)
103
+ for _ in range(iters):
104
+ # Assignment: BLAS GEMM path avoids the O(N*K*d) intermediate that
105
+ # torch.cdist allocates for small d (e.g. d=8 for n_sub=16).
106
+ assign = _sq_l2(data, cb).argmin(dim=1)
107
+
108
+ # Update: vectorized scatter_add replaces the Python loop over n_codes.
109
+ # Was: 256 Python iterations; now: 3 torch ops.
110
+ new_cb = torch.zeros_like(cb)
111
+ counts = torch.zeros(n_codes, dtype=data.dtype, device=dev)
112
+ new_cb.scatter_add_(0, assign.unsqueeze(1).expand(-1, d), data)
113
+ counts.scatter_add_(0, assign, ones)
114
+ live = counts > 0
115
+ new_cb[live] /= counts[live].unsqueeze(1)
116
+ # Re-seed dead centroids (LBG splitting heuristic)
117
+ dead = (~live).nonzero(as_tuple=True)[0]
118
+ if dead.numel() > 0:
119
+ ri = torch.randint(0, N, (dead.numel(),), generator=g).to(dev)
120
+ new_cb[dead] = data[ri]
121
+
122
+ shift = (new_cb - cb).norm()
123
+ cb = new_cb
124
+ if shift < 1e-5:
125
+ break
126
+ return cb
127
+
128
+
129
+ def lbg_codebook_batched(xb: torch.Tensor, n_codes: int, iters: int = 25,
130
+ seed: int = 0) -> torch.Tensor:
131
+ """Fit n_sub independent LBG codebooks in one batched pass.
132
+
133
+ Mirrors the structure of ProductVQKV._roundtrip: replaces the Python loop
134
+ over sub-blocks with a single bmm-based assignment and a scatter_add-based
135
+ update, so n_sub sub-block fits become one operation at each Lloyd step.
136
+
137
+ xb: (n_sub, N, sub_dim) training vectors, pre-normalized if needed
138
+ returns: (n_sub, n_codes, sub_dim) codebooks, one per sub-block
139
+ """
140
+ g = torch.Generator().manual_seed(seed)
141
+ n_sub, N, sub_dim = xb.shape
142
+ dev = xb.device
143
+
144
+ # Initialization: random subset per sub-block (same RNG sequence as serial)
145
+ idx = torch.stack([torch.randperm(N, generator=g)[:n_codes]
146
+ for _ in range(n_sub)]).to(dev) # (n_sub, K)
147
+ cb = xb[torch.arange(n_sub, device=dev).unsqueeze(1), idx].clone() # (n_sub, K, sub_dim)
148
+
149
+ ones = torch.ones(N, dtype=xb.dtype, device=dev)
150
+
151
+ for _ in range(iters):
152
+ # Assignment — one bmm replaces n_sub independent GEMMs
153
+ x_sq = (xb * xb).sum(-1, keepdim=True) # (n_sub, N, 1)
154
+ c_sq = (cb * cb).sum(-1).unsqueeze(1) # (n_sub, 1, K)
155
+ cross = torch.bmm(xb, cb.transpose(1, 2)) # (n_sub, N, K)
156
+ assign = (x_sq - 2 * cross + c_sq).argmin(dim=-1) # (n_sub, N)
157
+
158
+ # Update — batched scatter_add
159
+ new_cb = torch.zeros_like(cb)
160
+ counts = torch.zeros(n_sub, n_codes, dtype=xb.dtype, device=dev)
161
+ assign_exp = assign.unsqueeze(-1).expand(-1, -1, sub_dim)
162
+ new_cb.scatter_add_(1, assign_exp, xb)
163
+ counts.scatter_add_(1, assign, ones.unsqueeze(0).expand(n_sub, -1))
164
+
165
+ live = counts > 0
166
+ new_cb[live] /= counts[live].unsqueeze(-1)
167
+
168
+ # Re-seed dead centroids per sub-block (rare; loop is fine)
169
+ dead_any = ~live
170
+ if dead_any.any():
171
+ for s in range(n_sub):
172
+ dead = dead_any[s].nonzero(as_tuple=True)[0]
173
+ if dead.numel():
174
+ ri = torch.randint(0, N, (dead.numel(),), generator=g).to(dev)
175
+ new_cb[s, dead] = xb[s, ri]
176
+
177
+ shift = (new_cb - cb).norm()
178
+ cb = new_cb
179
+ if shift < 1e-5:
180
+ break
181
+
182
+ return cb
183
+
184
+
185
+ def _sq_l2(x: torch.Tensor, cb: torch.Tensor) -> torch.Tensor:
186
+ """Squared L2 distance matrix (N, K) via BLAS GEMM.
187
+
188
+ torch.cdist for small d (e.g. d=8 for n_sub=16) falls back to a naive
189
+ expand-and-subtract path that allocates an (N, K, d) intermediate.
190
+ For N=131072, K=256, d=8 that is 1.07 GB per call, causing massive
191
+ CPU allocation pressure and 100x slowdowns versus the BLAS path.
192
+
193
+ ||x-c||^2 = ||x||^2 - 2*(x @ c.T) + ||c||^2 uses only (N,K) memory.
194
+ """
195
+ return ((x * x).sum(1, keepdim=True)
196
+ + (cb * cb).sum(1)
197
+ - 2 * (x @ cb.T))
198
+
199
+
200
+ def vq_encode(x: torch.Tensor, cb: torch.Tensor) -> torch.Tensor:
201
+ """Nearest-codeword indices. x:(N,d) cb:(K,d) -> (N,) long."""
202
+ return _sq_l2(x, cb).argmin(dim=1)
203
+
204
+
205
+ # ----------------------------------------------------------------------------
206
+ # Product VQ
207
+ # ----------------------------------------------------------------------------
208
+ @dataclass
209
+ class ProductVQKV:
210
+ """Product vector quantization of head-dim sub-blocks.
211
+
212
+ Each head's `head_dim` vector is split into `n_sub` contiguous sub-vectors
213
+ of length `sub_dim = head_dim / n_sub`; each sub-vector is quantized
214
+ against its own codebook of size `n_codes`.
215
+
216
+ If `normalize=True`, each sub-vector is standardized by its per-dimension
217
+ calibration mean/std before codebook design and restored after decode.
218
+ This decorrelates the scale variation RoPE introduces in the rotated half
219
+ of the key, and is the mechanism that lets RoPE-split specialize.
220
+
221
+ Rate (bits/element) = n_sub * log2(n_codes) / head_dim.
222
+ e.g. head_dim=128, n_sub=8, n_codes=256 -> 8*8/128 = 0.5 bits/element.
223
+ """
224
+ n_sub: int = 8
225
+ n_codes: int = 256
226
+ iters: int = 25
227
+ normalize: bool = False
228
+ k_codebooks: list = field(default_factory=list)
229
+ v_codebooks: list = field(default_factory=list)
230
+ _k_stats: list = field(default_factory=list)
231
+ _v_stats: list = field(default_factory=list)
232
+ _k_stacked: tuple = None # lazily-built (cb, mu, sd) batched tensors
233
+ _v_stacked: tuple = None
234
+
235
+ def _split(self, x):
236
+ # x: (N, head_dim) -> list of (N, sub_dim)
237
+ return list(torch.chunk(x, self.n_sub, dim=-1))
238
+
239
+ def _fit_one(self, x):
240
+ N, head_dim = x.shape
241
+ sub_dim = head_dim // self.n_sub
242
+ # (n_sub, N, sub_dim) -- same layout _roundtrip uses, so batching mirrors inference
243
+ xb = x.reshape(N, self.n_sub, sub_dim).permute(1, 0, 2).contiguous()
244
+
245
+ if self.normalize:
246
+ mu = xb.mean(dim=1, keepdim=True) # (n_sub, 1, sub_dim)
247
+ sd = xb.std(dim=1, keepdim=True).clamp_min(1e-6)
248
+ xb = (xb - mu) / sd
249
+ # unstack into (1, sub_dim) tuples so _stack / to() are unchanged
250
+ stats = [(mu[s], sd[s]) for s in range(self.n_sub)]
251
+ else:
252
+ stats = [None] * self.n_sub
253
+
254
+ cb_batched = lbg_codebook_batched(xb, self.n_codes, self.iters)
255
+ cbs = list(cb_batched.unbind(dim=0)) # n_sub x (K, sub_dim)
256
+ return cbs, stats
257
+
258
+ def fit(self, k_calib, v_calib):
259
+ self.k_codebooks, self._k_stats = self._fit_one(k_calib)
260
+ self.v_codebooks, self._v_stats = self._fit_one(v_calib)
261
+ return self
262
+
263
+ def _stack(self, codebooks, stats):
264
+ """Lazily stack per-sub-block codebooks/stats into batched tensors so the
265
+ whole product-VQ encode is a few large ops instead of n_sub small ones.
266
+
267
+ Returns:
268
+ cb_stacked: (n_sub, K, sub_dim)
269
+ mu_stacked: (1, n_sub, sub_dim) or None (None => no normalization)
270
+ sd_stacked: (1, n_sub, sub_dim) or None
271
+ Requires uniform sub_dim and n_codes across sub-blocks, which ProductVQ
272
+ guarantees (torch.chunk into equal pieces, single n_codes).
273
+ """
274
+ cb_stacked = torch.stack(codebooks, dim=0) # (n_sub, K, sub_dim)
275
+ if any(st is not None for st in stats):
276
+ mu = torch.stack([st[0].reshape(-1) for st in stats], dim=0) # (n_sub, sub_dim)
277
+ sd = torch.stack([st[1].reshape(-1) for st in stats], dim=0)
278
+ mu_stacked = mu.unsqueeze(0) # (1, n_sub, sub_dim)
279
+ sd_stacked = sd.unsqueeze(0)
280
+ else:
281
+ mu_stacked = sd_stacked = None
282
+ return cb_stacked, mu_stacked, sd_stacked
283
+
284
+ def _ensure_stacked(self):
285
+ if getattr(self, "_k_stacked", None) is None:
286
+ self._k_stacked = self._stack(self.k_codebooks, self._k_stats)
287
+ if getattr(self, "_v_stacked", None) is None:
288
+ self._v_stacked = self._stack(self.v_codebooks, self._v_stats)
289
+
290
+ def _roundtrip(self, x, stacked):
291
+ """Batched product-VQ round-trip.
292
+
293
+ x: (N, head_dim). Splits into (N, n_sub, sub_dim), then does ONE batched
294
+ squared-L2 (n_sub, N, K), one argmin, one gather -- replacing the Python
295
+ loop over sub-blocks and its 3*n_sub small kernels.
296
+
297
+ Chunks over N so peak memory (the (n_sub, chunk, K) distance tensor)
298
+ stays bounded: at N=131072, n_sub=16, K=256 the un-chunked tensor is
299
+ ~2 GB in fp32. The batched path is a GPU optimization -- on CPU it is
300
+ roughly on par with the per-sub-block loop (no launch overhead to hide).
301
+ """
302
+ cb, mu, sd = stacked # cb: (n_sub, K, sub_dim)
303
+ n_sub, K, sub_dim = cb.shape
304
+ N = x.shape[0]
305
+ c_sq = (cb * cb).sum(-1).unsqueeze(1) # (n_sub, 1, K)
306
+ if mu is not None:
307
+ mu_b = mu.permute(1, 0, 2) # (n_sub, 1, sub_dim)
308
+ sd_b = sd.permute(1, 0, 2)
309
+
310
+ # cap the distance tensor at ~256 MB fp32: n_sub * chunk * K * 4 bytes
311
+ chunk = max(1, (256 * 1024 * 1024) // (n_sub * K * 4))
312
+ out_chunks = []
313
+ for start in range(0, N, chunk):
314
+ xc = x[start:start + chunk] # (c, head_dim)
315
+ c = xc.shape[0]
316
+ xb = xc.reshape(c, n_sub, sub_dim).permute(1, 0, 2).contiguous()
317
+ if mu is not None:
318
+ xb = (xb - mu_b) / sd_b
319
+ x_sq = (xb * xb).sum(-1, keepdim=True) # (n_sub, c, 1)
320
+ cross = torch.bmm(xb, cb.transpose(1, 2)) # (n_sub, c, K)
321
+ d2 = x_sq - 2 * cross + c_sq
322
+ idx = d2.argmin(dim=-1) # (n_sub, c)
323
+ idx_exp = idx.unsqueeze(-1).expand(-1, -1, sub_dim)
324
+ rec = torch.gather(cb, 1, idx_exp) # (n_sub, c, sub_dim)
325
+ if mu is not None:
326
+ rec = rec * sd_b + mu_b
327
+ out_chunks.append(rec.permute(1, 0, 2).reshape(c, n_sub * sub_dim))
328
+ return torch.cat(out_chunks, dim=0)
329
+
330
+ def roundtrip_k(self, k):
331
+ self._ensure_stacked()
332
+ return self._roundtrip(k, self._k_stacked)
333
+
334
+ def roundtrip_v(self, v):
335
+ self._ensure_stacked()
336
+ return self._roundtrip(v, self._v_stacked)
337
+
338
+ def to(self, device):
339
+ self.k_codebooks = [cb.to(device) for cb in self.k_codebooks]
340
+ self.v_codebooks = [cb.to(device) for cb in self.v_codebooks]
341
+ self._k_stats = [(st[0].to(device), st[1].to(device)) if st is not None else None
342
+ for st in self._k_stats]
343
+ self._v_stats = [(st[0].to(device), st[1].to(device)) if st is not None else None
344
+ for st in self._v_stats]
345
+ # invalidate cached stacks; they rebuild on next roundtrip on the new device
346
+ self._k_stacked = None
347
+ self._v_stacked = None
348
+ return self
349
+
350
+ def bits_per_element(self, head_dim):
351
+ sub_dim = head_dim / self.n_sub
352
+ return math.log2(self.n_codes) / sub_dim
353
+
354
+
355
+ @dataclass
356
+ class TurboQuantKV:
357
+ """Data-oblivious rotation + per-coordinate scalar quantization, in the
358
+ spirit of TurboQuant (Zandieh et al., ICLR 2026).
359
+
360
+ Pipeline: random rotation Pi (so coordinates become near-iid in high dim),
361
+ then a per-coordinate scalar codebook. We use a uniform Lloyd-Max-style
362
+ codebook on the rotated coordinates as a faithful stand-in for their
363
+ precomputed Beta-optimal codebook (the exact codebook is an implementation
364
+ detail; the rotation is the load-bearing idea). We store the per-vector L2
365
+ norm in fp16 and rescale on dequant, as the paper specifies.
366
+
367
+ This is included so we can compare a rotation-based SCALAR method against
368
+ product VQ on the attention-output COSINE metric -- the comparison the
369
+ TurboQuant paper does not make (it optimizes cache-vector MSE / inner prod).
370
+ """
371
+ nbits: int = 4
372
+ seed: int = 0
373
+ _rot: torch.Tensor = None
374
+ _levels: torch.Tensor = None
375
+
376
+ def _make_rotation(self, d):
377
+ g = torch.Generator().manual_seed(self.seed)
378
+ a = torch.randn(d, d, generator=g)
379
+ q, _ = torch.linalg.qr(a)
380
+ return q
381
+
382
+ def fit(self, k_calib, v_calib):
383
+ d = k_calib.shape[-1]
384
+ self._rot = self._make_rotation(d)
385
+ # Fit levels on UNIT-NORMALIZED, rotated coordinates -- the same domain
386
+ # the round-trip quantizes in. (Earlier bug: levels were fit on
387
+ # un-normalized rotated data, so the per-vector-normalized values fell
388
+ # outside the level range and collapsed to one bin.)
389
+ kn = k_calib / k_calib.norm(dim=-1, keepdim=True).clamp_min(1e-8)
390
+ rk = kn @ self._rot
391
+ lo = torch.quantile(rk.flatten()[:200000], 0.001)
392
+ hi = torch.quantile(rk.flatten()[:200000], 0.999)
393
+ self._levels = torch.linspace(lo.item(), hi.item(), (1 << self.nbits))
394
+ return self
395
+
396
+ def _roundtrip(self, x):
397
+ norms = x.norm(dim=-1, keepdim=True).clamp_min(1e-8)
398
+ xn = x / norms
399
+ y = xn @ self._rot # rotate
400
+ idx = torch.bucketize(y, self._levels)
401
+ idx = idx.clamp(0, self._levels.numel() - 1)
402
+ y_hat = self._levels[idx] # per-coord scalar quant
403
+ x_hat = y_hat @ self._rot.T # un-rotate
404
+ return x_hat * norms # rescale by stored norm
405
+
406
+ def roundtrip_k(self, k):
407
+ return self._roundtrip(k)
408
+
409
+ def roundtrip_v(self, v):
410
+ return self._roundtrip(v)
411
+
412
+ def to(self, device):
413
+ self._rot = self._rot.to(device)
414
+ self._levels = self._levels.to(device)
415
+ return self
416
+
417
+ def bits_per_element(self, head_dim):
418
+ # nbits/coord + fp16 norm amortized over head_dim
419
+ return self.nbits + 16 / head_dim
420
+
421
+
422
+ @dataclass
423
+ class RoPESplitVQKV:
424
+ """ProductVQ with separate codebooks for the RoPE'd vs pass-through halves
425
+ of each KEY head.
426
+
427
+ Laguna full-attention layers use partial_rotary_factor=0.5: the first half
428
+ of head_dim is rotated (position-dependent, broad distribution), the second
429
+ half is identity (position-independent). A single codebook must straddle
430
+ two regimes; splitting lets each codebook specialize.
431
+
432
+ Values receive no RoPE, so V uses a plain ProductVQ.
433
+ """
434
+ n_sub_half: int = 4 # sub-vectors PER HALF for keys
435
+ n_codes: int = 256
436
+ iters: int = 25
437
+ rotary_fraction: float = 0.5
438
+ _rope_vq: ProductVQKV = None
439
+ _pass_vq: ProductVQKV = None
440
+ _v_vq: ProductVQKV = None
441
+
442
+ def fit(self, k_calib, v_calib):
443
+ d = k_calib.shape[-1]
444
+ cut = int(d * self.rotary_fraction)
445
+ k_rope, k_pass = k_calib[..., :cut], k_calib[..., cut:]
446
+ self._cut = cut
447
+ self._rope_vq = ProductVQKV(self.n_sub_half, self.n_codes, self.iters,
448
+ normalize=True).fit(k_rope, k_rope)
449
+ self._pass_vq = ProductVQKV(self.n_sub_half, self.n_codes, self.iters,
450
+ normalize=False).fit(k_pass, k_pass)
451
+ self._v_vq = ProductVQKV(2 * self.n_sub_half, self.n_codes, self.iters,
452
+ normalize=True).fit(v_calib, v_calib)
453
+ return self
454
+
455
+ def roundtrip_k(self, k):
456
+ kr = self._rope_vq.roundtrip_k(k[..., :self._cut])
457
+ kp = self._pass_vq.roundtrip_k(k[..., self._cut:])
458
+ return torch.cat([kr, kp], dim=-1)
459
+
460
+ def roundtrip_v(self, v):
461
+ return self._v_vq.roundtrip_v(v)
462
+
463
+ def to(self, device):
464
+ self._rope_vq.to(device)
465
+ self._pass_vq.to(device)
466
+ self._v_vq.to(device)
467
+ return self
468
+
469
+ def bits_per_element(self, head_dim):
470
+ # both halves use n_sub_half codes over head_dim/2 elements
471
+ half = head_dim / 2
472
+ sub_dim = half / self.n_sub_half
473
+ return math.log2(self.n_codes) / sub_dim
474
+
475
+
476
+ # ----------------------------------------------------------------------------
477
+ # Data-oblivious baseline: a simplified TurboQuant-style quantizer
478
+ # ----------------------------------------------------------------------------
479
+ @dataclass
480
+ class RandomRotationScalarKV:
481
+ """Simplified, data-OBLIVIOUS rotation-then-scalar quantizer in the spirit
482
+ of TurboQuant / PolarQuant (Zandieh et al., ICLR 2026).
483
+
484
+ NOT the full method: TurboQuant adds PolarQuant's normalization-free polar
485
+ transform and a 1-bit QJL residual correction for UNBIASED inner-product
486
+ estimation. This stand-in captures only the core data-oblivious idea --
487
+ apply a fixed random orthogonal rotation so coordinates concentrate (a
488
+ Beta/Gaussian-like distribution), then scalar-quantize each coordinate with
489
+ a fixed range. It exists so the harness can run the central scientific
490
+ comparison of this project:
491
+
492
+ data-OBLIVIOUS rotation+scalar vs. data-DEPENDENT product VQ
493
+
494
+ on real Laguna cache statistics. If you want the real thing, drop in the
495
+ unofficial impl (github.com/0xSero/turboquant or hackimov/turboquant-kv)
496
+ behind this same fit/roundtrip interface. The OpenReview discussion of the
497
+ paper is contested precisely on the oblivious-vs-data-dependent claim, so a
498
+ clean head-to-head on a NEW model is a genuine contribution either way.
499
+ """
500
+ nbits: int = 3
501
+ seed: int = 0
502
+ _R: torch.Tensor = None # rotation
503
+ _Rk_range: tuple = None
504
+ _Rv_range: tuple = None
505
+
506
+ def _rotation(self, d):
507
+ g = torch.Generator().manual_seed(self.seed)
508
+ a = torch.randn(d, d, generator=g)
509
+ q, _ = torch.linalg.qr(a) # random orthogonal matrix
510
+ return q
511
+
512
+ def fit(self, k_calib, v_calib):
513
+ d = k_calib.shape[-1]
514
+ self._R = self._rotation(d)
515
+ # fixed (data-oblivious-ish) ranges from a robust quantile of rotated calib
516
+ rk = k_calib @ self._R
517
+ rv = v_calib @ self._R
518
+ self._Rk_range = (rk.quantile(0.001), rk.quantile(0.999))
519
+ self._Rv_range = (rv.quantile(0.001), rv.quantile(0.999))
520
+ return self
521
+
522
+ def _rt(self, x, rng):
523
+ xr = x @ self._R
524
+ lo, hi = rng
525
+ qmax = (1 << self.nbits) - 1
526
+ scale = (hi - lo).clamp_min(1e-8) / qmax
527
+ q = torch.clamp(torch.round((xr - lo) / scale), 0, qmax)
528
+ xr_hat = q * scale + lo
529
+ return xr_hat @ self._R.T # rotation is orthogonal, inverse == transpose
530
+
531
+ def roundtrip_k(self, k):
532
+ return self._rt(k, self._Rk_range)
533
+
534
+ def roundtrip_v(self, v):
535
+ return self._rt(v, self._Rv_range)
536
+
537
+ def bits_per_element(self, head_dim):
538
+ # rotation is a fixed matrix (no per-token overhead); ranges are global.
539
+ return float(self.nbits)
540
+
541
+
542
+ # ----------------------------------------------------------------------------
543
+ # 1-bit baselines (the floor of the scalar family; rate-neighbors to VQ)
544
+ # ----------------------------------------------------------------------------
545
+ @dataclass
546
+ class SignScalarKV:
547
+ """Symmetric 1-bit (sign) quantization with a per-group scale.
548
+
549
+ x_hat = scale * sign(x), scale = mean(|x|) over the group.
550
+
551
+ This is the honest 1-bit floor of the KIVI/quanto scalar family: unlike the
552
+ affine `ScalarKV(nbits=1)`, it stores NO zero-point (cache K/V are ~zero-mean
553
+ after norm), so it is both cheaper and better-centered. Group axis matches
554
+ KIVI conventions: keys per-channel (token axis), values per-token.
555
+ `per_channel_key` toggles the key axis.
556
+ """
557
+ per_channel_key: bool = True
558
+ group_dim_k: int = 0 # 0 = per-channel (token axis); -1 = per-token
559
+ bits: float = 1.0
560
+
561
+ def fit(self, k_calib, v_calib):
562
+ return self
563
+
564
+ def _sign_q(self, x, dim):
565
+ scale = x.abs().mean(dim=dim, keepdim=True).clamp_min(1e-8)
566
+ return torch.sign(x) * scale
567
+
568
+ def roundtrip_k(self, k):
569
+ dim = 0 if self.per_channel_key else -1
570
+ return self._sign_q(k, dim)
571
+
572
+ def roundtrip_v(self, v):
573
+ return self._sign_q(v, dim=-1)
574
+
575
+ def bits_per_element(self, head_dim):
576
+ # 1 bit/element + one fp16 scale per group amortized over the group.
577
+ # per-token value group = head_dim elements; per-channel key group is the
578
+ # token axis (large), so its scale overhead is negligible. Report ~1 +
579
+ # 16/head_dim as a conservative upper bound for the per-token case.
580
+ return 1.0 + 16.0 / head_dim
581
+
582
+
583
+ @dataclass
584
+ class TernaryScalarKV:
585
+ """1.58-bit ternary quantization {-1, 0, +1} with a per-group scale, in the
586
+ style of BitNet b1.58 -- the "just go (almost) 1-bit" school that the
587
+ Bonsai/PrismML line popularized. Zeros are assigned by a threshold at a
588
+ fraction of the mean-abs, letting small-magnitude coordinates drop out.
589
+
590
+ thr = alpha * mean(|x|); x_hat = scale * {sign(x) if |x|>thr else 0}
591
+
592
+ Rate ~ log2(3) ~ 1.58 bits/element. Included so the table spans the full
593
+ aggressive regime and so a reviewer who knows BitNet sees the comparison.
594
+ """
595
+ alpha: float = 0.7
596
+ per_channel_key: bool = True
597
+
598
+ def fit(self, k_calib, v_calib):
599
+ return self
600
+
601
+ def _tern_q(self, x, dim):
602
+ m = x.abs().mean(dim=dim, keepdim=True).clamp_min(1e-8)
603
+ thr = self.alpha * m
604
+ mask = (x.abs() > thr).to(x.dtype)
605
+ scale = (x.abs() * mask).sum(dim=dim, keepdim=True) / \
606
+ mask.sum(dim=dim, keepdim=True).clamp_min(1.0)
607
+ return torch.sign(x) * mask * scale
608
+
609
+ def roundtrip_k(self, k):
610
+ dim = 0 if self.per_channel_key else -1
611
+ return self._tern_q(k, dim)
612
+
613
+ def roundtrip_v(self, v):
614
+ return self._tern_q(v, dim=-1)
615
+
616
+ def bits_per_element(self, head_dim):
617
+ import math as _m
618
+ return _m.log2(3) + 16.0 / head_dim