nexusquant-kv 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nexusquant/__init__.py ADDED
@@ -0,0 +1,32 @@
1
+ """NexusQuant: Training-Free KV Cache Compression via E8 Lattice VQ + Token Eviction.
2
+
3
+ GPU-validated numbers (Mistral-7B, A100, all overhead included):
4
+ 10.4x at +0.43% PPL (quality="high", 35% eviction)
5
+ 16.8x at +1.34% PPL (quality="balanced", 60% eviction)
6
+ 33.3x at +2.64% PPL (quality="max", 80% eviction)
7
+
8
+ Training-free. Zero calibration. One line of code. Apache 2.0.
9
+
10
+ Quick start:
11
+ from nexusquant import nexusquant_evict
12
+
13
+ with nexusquant_evict(model, quality="balanced"):
14
+ output = model.generate(input_ids, max_new_tokens=100)
15
+ """
16
+
17
+ __version__ = "0.4.0"
18
+
19
+ from nexusquant.pipeline import (
20
+ NexusQuantFast,
21
+ NexusQuantSimple,
22
+ NexusQuantMax,
23
+ NexusQuantEvict,
24
+ compress_kv_cache,
25
+ )
26
+
27
+ from nexusquant.integrations.huggingface import (
28
+ nexusquant,
29
+ nexusquant_simple,
30
+ nexusquant_max,
31
+ nexusquant_evict,
32
+ )
@@ -0,0 +1,40 @@
1
+ from nexusquant.core.e8_lattice import E8Lattice
2
+ from nexusquant.core.hadamard import hadamard_matrix, fht, ifht
3
+ from nexusquant.core.dp_allocator import (
4
+ dp_bit_allocation,
5
+ calibrate_distortion_factors,
6
+ DISTORTION_THEORETICAL,
7
+ DISTORTION_EMPIRICAL,
8
+ )
9
+ from nexusquant.core.rope_utils import inverse_rope, forward_rope
10
+ from nexusquant.core.token_merger import merge_tokens, merge_and_drop
11
+ from nexusquant.core.entropy_coder import (
12
+ encode_e8,
13
+ decode_e8,
14
+ measure_entropy,
15
+ measure_e8_entropy,
16
+ e8_quantize_with_entropy,
17
+ )
18
+ from nexusquant.core.temporal_codec import (
19
+ compress_indices,
20
+ decompress_indices,
21
+ temporal_delta_encode,
22
+ temporal_delta_decode,
23
+ measure_compression,
24
+ )
25
+ from nexusquant.core.nsn import forward_nsn, inverse_nsn, NSNStats
26
+ from nexusquant.core.tcc import (
27
+ forward_tcc,
28
+ inverse_tcc,
29
+ compute_compression_stats,
30
+ TCCCompressed,
31
+ )
32
+ from nexusquant.core.optimal_shrinkage import (
33
+ estimate_noise_sigma,
34
+ optimal_shrinkage_frobenius,
35
+ optimal_hard_threshold,
36
+ effective_dims,
37
+ variance_retained,
38
+ reconstruct_with_shrinkage,
39
+ mp_median,
40
+ )
@@ -0,0 +1,372 @@
1
+ """Byte-accurate compression measurement for E8-quantized KV cache.
2
+
3
+ Every overhead is counted:
4
+ - E8 integer coordinates (int8)
5
+ - Per-head fp16 scales
6
+ - Temporal delta + zstd compressed index stream
7
+ - PCA basis and mean vectors (if used)
8
+ - A fixed header that records the config needed to decode
9
+
10
+ No rounding tricks. No partial counts.
11
+ """
12
+
13
+ import struct
14
+ import numpy as np
15
+ import torch
16
+ import zstandard
17
+
18
+ from nexusquant.core.e8_lattice import E8Lattice
19
+ from nexusquant.core.hadamard import hadamard_matrix
20
+ from nexusquant.core.rope_utils import inverse_rope
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Header layout (24 bytes, always present)
25
+ # [magic:4B] [n_layers:2B] [n_heads:2B] [head_dim:2B] [n_tokens:4B]
26
+ # [bits:1B] [pca_dims:2B] [use_delta:1B] [zstd_level:1B] [reserved:5B]
27
+ # ---------------------------------------------------------------------------
28
+ _HEADER_MAGIC = b'NQC1'
29
+ _HEADER_SIZE = 24 # bytes
30
+
31
+
32
+ def _pack_header(n_layers, n_heads, head_dim, n_tokens,
33
+ bits, pca_dims, use_delta, zstd_level):
34
+ pca = pca_dims if pca_dims is not None else 0
35
+ return struct.pack(
36
+ '<4sHHHIBHBBxxxxx',
37
+ _HEADER_MAGIC,
38
+ n_layers,
39
+ n_heads,
40
+ head_dim,
41
+ n_tokens,
42
+ bits,
43
+ pca,
44
+ int(use_delta),
45
+ zstd_level,
46
+ )
47
+
48
+
49
+ def _compress_with_delta_zstd(coords_int8, n_tokens, n_vectors_per_token, zstd_level=22):
50
+ """Temporal delta + zstd on a flat int8 coordinate array.
51
+
52
+ coords_int8: flat int8 array of all E8 coordinates
53
+ n_tokens: sequence length
54
+ n_vectors_per_token: n_layers * 2 * n_heads
55
+ """
56
+ coords_per_vector = len(coords_int8) // (n_tokens * n_vectors_per_token)
57
+ reshaped = coords_int8.reshape(n_vectors_per_token, n_tokens, coords_per_vector)
58
+ delta = np.zeros_like(reshaped)
59
+ delta[:, 0, :] = reshaped[:, 0, :]
60
+ delta[:, 1:, :] = reshaped[:, 1:, :] - reshaped[:, :-1, :]
61
+ cctx = zstandard.ZstdCompressor(level=zstd_level)
62
+ return cctx.compress(delta.astype(np.int8).tobytes())
63
+
64
+
65
+ def _compress_raw_zstd(coords_int8, zstd_level=22):
66
+ cctx = zstandard.ZstdCompressor(level=zstd_level)
67
+ return cctx.compress(coords_int8.astype(np.int8).tobytes())
68
+
69
+
70
+ def _decompress_with_delta(compressed, n_tokens, n_vectors_per_token, n_coords):
71
+ dctx = zstandard.ZstdDecompressor()
72
+ raw = dctx.decompress(compressed)
73
+ coords_per_vector = n_coords // (n_tokens * n_vectors_per_token)
74
+ delta = np.frombuffer(raw, dtype=np.int8).copy()
75
+ reshaped = delta.reshape(n_vectors_per_token, n_tokens, coords_per_vector)
76
+ recovered = np.cumsum(reshaped, axis=1).astype(np.int8)
77
+ return recovered.reshape(-1)
78
+
79
+
80
+ def _decompress_raw(compressed):
81
+ dctx = zstandard.ZstdDecompressor()
82
+ raw = dctx.decompress(compressed)
83
+ return np.frombuffer(raw, dtype=np.int8).copy()
84
+
85
+
86
+ def _apply_pca(vectors_2d, pca_dims):
87
+ """Fit PCA and return (projected, Vh, mean).
88
+
89
+ vectors_2d: (N, head_dim) numpy array
90
+ Returns projected (N, pca_dims), Vh (pca_dims, head_dim), mean (head_dim,)
91
+ all as fp32 numpy.
92
+ """
93
+ mean = vectors_2d.mean(axis=0)
94
+ centered = vectors_2d - mean
95
+ # SVD-based PCA
96
+ _, _, Vh = np.linalg.svd(centered, full_matrices=False)
97
+ Vh_trunc = Vh[:pca_dims] # (pca_dims, head_dim)
98
+ projected = centered @ Vh_trunc.T # (N, pca_dims)
99
+ return projected, Vh_trunc, mean
100
+
101
+
102
+ def measure_compression(
103
+ past_key_values,
104
+ bits: int = 3,
105
+ head_dim: int = 64,
106
+ pca_dims: int = None,
107
+ use_temporal_delta: bool = True,
108
+ zstd_level: int = 22,
109
+ ) -> dict:
110
+ """Measure exact compression ratio with full overhead accounting.
111
+
112
+ Parameters
113
+ ----------
114
+ past_key_values : DynamicCache from HuggingFace
115
+ pkv.key_cache[layer] = tensor (1, n_heads, seq_len, head_dim)
116
+ bits : int
117
+ E8 quantization bits. 2 => levels=4, 3 => levels=8.
118
+ head_dim : int
119
+ Head dimension (auto-detected if cache tensors have enough dims).
120
+ pca_dims : int or None
121
+ If set, project each KV head down to this many dimensions before
122
+ quantization. PCA is fit on the same data (in-sample).
123
+ use_temporal_delta : bool
124
+ Whether to apply temporal delta coding before zstd.
125
+ zstd_level : int
126
+ zstd compression level (1-22).
127
+
128
+ Returns
129
+ -------
130
+ dict with:
131
+ fp16_bytes - original FP16 byte count
132
+ index_raw_bytes - E8 coordinate bytes before entropy coding
133
+ index_compressed_bytes - after temporal delta + zstd
134
+ scale_bytes - fp16 per-vector scales
135
+ pca_basis_bytes - PCA Vh + mean (0 if no PCA)
136
+ header_bytes - fixed metadata header
137
+ total_compressed_bytes - sum of all compressed components
138
+ compression_ratio - fp16_bytes / total_compressed_bytes
139
+ bits_per_dim - total_compressed_bytes * 8 / total_elements
140
+ breakdown - human-readable per-component string
141
+ """
142
+ levels = 2 ** bits
143
+
144
+ # -----------------------------------------------------------------------
145
+ # 1. Collect cache tensors
146
+ # Support both old DynamicCache (.key_cache list) and new DynamicCache
147
+ # (.to_legacy_cache() -> tuple of (K, V) tuples) and raw legacy tuples.
148
+ # -----------------------------------------------------------------------
149
+ if isinstance(past_key_values, tuple):
150
+ # Already in legacy format: tuple of (K_tensor, V_tensor)
151
+ legacy = past_key_values
152
+ elif hasattr(past_key_values, 'key_cache'):
153
+ # Old DynamicCache API
154
+ legacy = tuple(
155
+ (past_key_values.key_cache[i], past_key_values.value_cache[i])
156
+ for i in range(len(past_key_values.key_cache))
157
+ )
158
+ else:
159
+ # New DynamicCache API (transformers >= 4.45)
160
+ legacy = past_key_values.to_legacy_cache()
161
+
162
+ n_layers = len(legacy)
163
+ # Auto-detect dimensions from the first layer K tensor
164
+ ref = legacy[0][0] # (1, n_heads, n_tokens, head_dim)
165
+ if ref.dim() == 4:
166
+ n_heads = ref.shape[1]
167
+ n_tokens = ref.shape[2]
168
+ head_dim_actual = ref.shape[3]
169
+ else:
170
+ # (n_heads, n_tokens, head_dim) without batch dim
171
+ n_heads = ref.shape[0]
172
+ n_tokens = ref.shape[1]
173
+ head_dim_actual = ref.shape[2]
174
+
175
+ # Use the actual head_dim from the cache rather than the parameter default
176
+ head_dim = head_dim_actual
177
+
178
+ # Convenience accessors
179
+ def _get_kv(layer_idx, is_value):
180
+ t = legacy[layer_idx][1 if is_value else 0]
181
+ if t.dim() == 4:
182
+ return t.squeeze(0) # (n_heads, n_tokens, head_dim)
183
+ return t # already (n_heads, n_tokens, head_dim)
184
+
185
+ # -----------------------------------------------------------------------
186
+ # 2. Original FP16 byte count
187
+ # -----------------------------------------------------------------------
188
+ total_elements = n_layers * 2 * n_heads * n_tokens * head_dim
189
+ fp16_bytes = total_elements * 2 # 2 bytes per fp16 element
190
+
191
+ # -----------------------------------------------------------------------
192
+ # 3. Header bytes (fixed, always written)
193
+ # -----------------------------------------------------------------------
194
+ header = _pack_header(
195
+ n_layers, n_heads, head_dim, n_tokens,
196
+ bits, pca_dims, use_temporal_delta, zstd_level
197
+ )
198
+ assert len(header) == _HEADER_SIZE
199
+ header_bytes = _HEADER_SIZE
200
+
201
+ # -----------------------------------------------------------------------
202
+ # 4. Per-vector fp16 scales
203
+ # One scale per (layer, K/V, head, token) vector.
204
+ # For PCA: scale is on the projected vector, so same count.
205
+ # -----------------------------------------------------------------------
206
+ n_vectors = n_layers * 2 * n_heads * n_tokens
207
+ scale_bytes = n_vectors * 2 # one fp16 per vector
208
+
209
+ # -----------------------------------------------------------------------
210
+ # 5. PCA basis bytes (per layer, per K/V, per head - independent basis)
211
+ # Vh : (pca_dims, head_dim) fp16 per head per K/V per layer
212
+ # mean: (head_dim,) fp16 per head per K/V per layer
213
+ # -----------------------------------------------------------------------
214
+ if pca_dims is not None:
215
+ # Each head in each layer gets its own PCA basis
216
+ n_bases = n_layers * 2 * n_heads
217
+ vh_bytes_each = pca_dims * head_dim * 2 # fp16
218
+ mean_bytes_each = head_dim * 2 # fp16
219
+ pca_basis_bytes = n_bases * (vh_bytes_each + mean_bytes_each)
220
+ quant_dim = pca_dims # quantize in projected space
221
+ else:
222
+ pca_basis_bytes = 0
223
+ quant_dim = head_dim
224
+
225
+ # -----------------------------------------------------------------------
226
+ # 6. Quantize and extract integer coordinates
227
+ # -----------------------------------------------------------------------
228
+ # coords_per_vector: how many int8 values per input vector after E8
229
+ # E8 groups 8 dims; each group yields 8 int8 coordinates
230
+ groups_per_vector = (quant_dim + 7) // 8 # ceil, padding handled inside E8Lattice
231
+ coords_per_vector = groups_per_vector * 8
232
+
233
+ all_coords = [] # list of int8 arrays, one per (layer, kv, head, token) vector
234
+ all_scales = [] # float, will store as fp16
235
+
236
+ for layer_idx in range(n_layers):
237
+ for kv_idx in range(2): # 0=key, 1=value
238
+ tensor = _get_kv(layer_idx, is_value=(kv_idx == 1))
239
+ # tensor: (n_heads, n_tokens, head_dim)
240
+
241
+ # Remove RoPE from keys before compression (as per best practice)
242
+ if kv_idx == 0:
243
+ try:
244
+ tensor = inverse_rope(tensor) # (n_heads, n_tokens, head_dim)
245
+ except Exception:
246
+ pass # skip if RoPE removal fails (e.g. non-Llama model)
247
+
248
+ # Work in float32 for numerical accuracy
249
+ tensor_f32 = tensor.float() # (n_heads, n_tokens, head_dim)
250
+
251
+ for head_idx in range(n_heads):
252
+ head_data = tensor_f32[head_idx] # (n_tokens, head_dim)
253
+ vectors = head_data.numpy() # (n_tokens, head_dim) float32
254
+
255
+ if pca_dims is not None:
256
+ projected, Vh, mean = _apply_pca(vectors, pca_dims)
257
+ vectors = projected.astype(np.float32) # (n_tokens, pca_dims)
258
+
259
+ # Convert to torch for E8Lattice
260
+ v_torch = torch.from_numpy(vectors) # (n_tokens, quant_dim)
261
+
262
+ # Per-vector (per-token) scaling
263
+ amax = v_torch.abs().amax(dim=-1, keepdim=True).clamp(min=1e-8)
264
+ scale = amax / (levels / 2) # (n_tokens, 1)
265
+
266
+ # Normalize and quantize
267
+ normalized = v_torch / scale # (n_tokens, quant_dim)
268
+
269
+ # Pad to multiple of 8 for E8 groups
270
+ pad = (8 - quant_dim % 8) % 8
271
+ if pad > 0:
272
+ normalized = torch.nn.functional.pad(normalized, (0, pad))
273
+
274
+ # E8 nearest point per group
275
+ flat = normalized.reshape(-1, 8) # (n_tokens * groups_per_vector, 8)
276
+ lp = E8Lattice.nearest_point(flat).clamp(-levels / 2, levels / 2)
277
+
278
+ # Integer coordinates: multiply back by levels/2 to get integers
279
+ # lp values are multiples of 0.5 or 1.0; rounding is exact
280
+ coords_float = lp * (levels / 2) # [-levels/2*levels/2 ... ] no, think again
281
+ # lp is already in integer-or-half-integer lattice units
282
+ # The actual integer index is round(lp): lp is the lattice point
283
+ # in normalized space where scale = 1/(levels/2), so lp * (levels/2)
284
+ # gives the integer coordinate in [-levels/2, levels/2]
285
+ coords_int = coords_float.round().to(torch.int8).numpy()
286
+ # Shape: (n_tokens * groups_per_vector * 8,)
287
+ # Equivalently (n_tokens, groups_per_vector * 8) = (n_tokens, coords_per_vector)
288
+
289
+ all_coords.append(coords_int.reshape(-1))
290
+ all_scales.append(scale.squeeze(-1).numpy()) # (n_tokens,) fp32
291
+
292
+ # -----------------------------------------------------------------------
293
+ # 7. Concatenate and measure raw index bytes
294
+ # -----------------------------------------------------------------------
295
+ all_coords_flat = np.concatenate(all_coords, dtype=np.int8) # total int8 count
296
+ index_raw_bytes = len(all_coords_flat) # 1 byte per int8 element
297
+
298
+ # -----------------------------------------------------------------------
299
+ # 8. Compress indices: temporal delta + zstd
300
+ # Layout for delta: (n_vectors_per_token, n_tokens, coords_per_vector)
301
+ # n_vectors_per_token = n_layers * 2 * n_heads
302
+ # -----------------------------------------------------------------------
303
+ n_vectors_per_token = n_layers * 2 * n_heads
304
+
305
+ if use_temporal_delta:
306
+ compressed_indices = _compress_with_delta_zstd(
307
+ all_coords_flat, n_tokens, n_vectors_per_token, zstd_level
308
+ )
309
+ else:
310
+ compressed_indices = _compress_raw_zstd(all_coords_flat, zstd_level)
311
+
312
+ index_compressed_bytes = len(compressed_indices)
313
+
314
+ # -----------------------------------------------------------------------
315
+ # 9. Round-trip verification
316
+ # -----------------------------------------------------------------------
317
+ if use_temporal_delta:
318
+ recovered = _decompress_with_delta(
319
+ compressed_indices, n_tokens, n_vectors_per_token, len(all_coords_flat)
320
+ )
321
+ else:
322
+ recovered = _decompress_raw(compressed_indices)
323
+
324
+ if not np.array_equal(all_coords_flat, recovered):
325
+ raise RuntimeError(
326
+ f"Round-trip verification FAILED: "
327
+ f"{np.sum(all_coords_flat != recovered)} mismatches out of {len(all_coords_flat)}"
328
+ )
329
+
330
+ # -----------------------------------------------------------------------
331
+ # 10. Total and ratios
332
+ # -----------------------------------------------------------------------
333
+ total_compressed_bytes = (
334
+ header_bytes
335
+ + index_compressed_bytes
336
+ + scale_bytes
337
+ + pca_basis_bytes
338
+ )
339
+
340
+ compression_ratio = fp16_bytes / total_compressed_bytes
341
+ bits_per_dim = (total_compressed_bytes * 8) / total_elements
342
+
343
+ # -----------------------------------------------------------------------
344
+ # 11. Human-readable breakdown
345
+ # -----------------------------------------------------------------------
346
+ def pct(b):
347
+ return f"{b / total_compressed_bytes * 100:.1f}%"
348
+
349
+ breakdown = (
350
+ f"FP16 original: {fp16_bytes:>10,} bytes\n"
351
+ f" header: {header_bytes:>10,} bytes ({pct(header_bytes)})\n"
352
+ f" index raw: {index_raw_bytes:>10,} bytes (uncompressed)\n"
353
+ f" index compressed: {index_compressed_bytes:>10,} bytes ({pct(index_compressed_bytes)})\n"
354
+ f" scales (fp16): {scale_bytes:>10,} bytes ({pct(scale_bytes)})\n"
355
+ f" pca basis: {pca_basis_bytes:>10,} bytes ({pct(pca_basis_bytes)})\n"
356
+ f" TOTAL compressed: {total_compressed_bytes:>10,} bytes\n"
357
+ f" ratio: {compression_ratio:>10.3f}x\n"
358
+ f" bits/dim: {bits_per_dim:>10.3f}"
359
+ )
360
+
361
+ return {
362
+ 'fp16_bytes': fp16_bytes,
363
+ 'index_raw_bytes': index_raw_bytes,
364
+ 'index_compressed_bytes': index_compressed_bytes,
365
+ 'scale_bytes': scale_bytes,
366
+ 'pca_basis_bytes': pca_basis_bytes,
367
+ 'header_bytes': header_bytes,
368
+ 'total_compressed_bytes': total_compressed_bytes,
369
+ 'compression_ratio': compression_ratio,
370
+ 'bits_per_dim': bits_per_dim,
371
+ 'breakdown': breakdown,
372
+ }
@@ -0,0 +1,161 @@
1
+ """Dynamic programming optimal bit allocation for PCA dimensions.
2
+
3
+ Given eigenvalues from PCA decomposition and a total bit budget,
4
+ allocates {0,1,2,3,4} bits per dimension to minimize total
5
+ weighted distortion: sum(eigenvalue_i * distortion_factor(bits_i)).
6
+
7
+ Supports both theoretical (Gaussian assumption) and empirical distortion
8
+ factors. Empirical factors are 1.5-2.4x lower because E8 lattice VQ
9
+ performs better on real KV cache data than Gaussian theory predicts.
10
+ """
11
+
12
+ from typing import List, Optional, Dict
13
+ import numpy as np
14
+
15
+
16
+ # Theoretical: Gaussian-optimal distortion factors (standard rate-distortion)
17
+ DISTORTION_THEORETICAL = {0: 1.0, 1: 0.363, 2: 0.117, 3: 0.034, 4: 0.0095}
18
+
19
+ # Empirical: Measured on Llama-3.1-8B KV cache after Hadamard + E8
20
+ # E8 is 1.5-2.4x better than Gaussian theory on real KV data
21
+ DISTORTION_EMPIRICAL = {0: 1.0, 1: 0.244, 2: 0.063, 3: 0.016, 4: 0.004}
22
+
23
+ # Legacy alias
24
+ DISTORTION_FACTORS = DISTORTION_THEORETICAL
25
+
26
+
27
+ def marchenko_pastur_threshold(eigenvalues: np.ndarray, n_samples: int, n_dims: int) -> int:
28
+ """Compute signal dimension count using Marchenko-Pastur noise edge.
29
+
30
+ Random Matrix Theory: for random (n, p) data with noise variance sigma^2,
31
+ the bulk eigenvalue distribution has upper edge sigma^2 * (1 + sqrt(p/n))^2.
32
+ Eigenvalues above this edge are signal; below are noise (safe to drop).
33
+
34
+ Returns the number of signal dimensions.
35
+ """
36
+ gamma = n_dims / max(n_samples, 1)
37
+ sigma2 = float(np.median(eigenvalues))
38
+ edge = sigma2 * (1 + np.sqrt(gamma)) ** 2
39
+ return int(np.sum(eigenvalues > edge))
40
+
41
+
42
+ def select_distortion_factors(
43
+ eigenvalues: np.ndarray,
44
+ bits_per_dim: float,
45
+ ) -> Dict[int, float]:
46
+ """Adaptive distortion selection by bitrate and effective rank.
47
+
48
+ Universal rule validated on 4 model families:
49
+ - <=2.0 bpd (>=8x): Always empirical
50
+ - 2.5 bpd (6.4x): Empirical (safe default)
51
+ - >=3.0 bpd (<=5.3x): Auto-select by rank (rank>40 -> empirical, else theoretical)
52
+ """
53
+ rank = int(np.sum(eigenvalues > eigenvalues.max() * 0.01))
54
+ if bits_per_dim <= 2.5:
55
+ return DISTORTION_EMPIRICAL
56
+ if rank > 40:
57
+ return DISTORTION_EMPIRICAL
58
+ return DISTORTION_THEORETICAL
59
+
60
+
61
+ def dp_bit_allocation(
62
+ eigenvalues: np.ndarray,
63
+ total_budget: int,
64
+ max_bits: int = 4,
65
+ distortion: str = "auto",
66
+ custom_factors: Optional[Dict[int, float]] = None,
67
+ ) -> List[int]:
68
+ """Compute optimal bit allocation via dynamic programming.
69
+
70
+ Args:
71
+ eigenvalues: PCA eigenvalues (variance per dimension), shape (n_dims,)
72
+ total_budget: Total bits to allocate across all dimensions
73
+ max_bits: Maximum bits per dimension (default 4)
74
+ distortion: "theoretical", "empirical", or "auto" (adaptive selection)
75
+ custom_factors: Override with custom distortion factors dict
76
+
77
+ Returns:
78
+ List of bit allocations, one per dimension
79
+ """
80
+ n = len(eigenvalues)
81
+ bits_per_dim = total_budget / max(n, 1)
82
+
83
+ if custom_factors is not None:
84
+ factors = custom_factors
85
+ elif distortion == "empirical":
86
+ factors = DISTORTION_EMPIRICAL
87
+ elif distortion == "theoretical":
88
+ factors = DISTORTION_THEORETICAL
89
+ else: # auto
90
+ factors = select_distortion_factors(eigenvalues, bits_per_dim)
91
+
92
+ dp = [[float('inf')] * (total_budget + 1) for _ in range(n + 1)]
93
+ choice = [[0] * (total_budget + 1) for _ in range(n + 1)]
94
+ dp[0][0] = 0
95
+
96
+ for i in range(1, n + 1):
97
+ lam = max(float(eigenvalues[i - 1]), 1e-10)
98
+ for b in range(total_budget + 1):
99
+ for bits in range(min(max_bits + 1, b + 1)):
100
+ cost = lam * factors[bits]
101
+ if dp[i - 1][b - bits] + cost < dp[i][b]:
102
+ dp[i][b] = dp[i - 1][b - bits] + cost
103
+ choice[i][b] = bits
104
+
105
+ alloc = []
106
+ b = total_budget
107
+ for i in range(n, 0, -1):
108
+ alloc.append(choice[i][b])
109
+ b -= choice[i][b]
110
+ return list(reversed(alloc))
111
+
112
+
113
+ def calibrate_distortion_factors(
114
+ data: np.ndarray,
115
+ head_dim: int = 128,
116
+ max_bits: int = 4,
117
+ ) -> Dict[int, float]:
118
+ """Calibrate empirical distortion factors from real KV cache data.
119
+
120
+ Measures actual E8 quantization distortion at each bitrate on provided data,
121
+ producing model-specific factors that are typically 1.5-2.4x lower than theory.
122
+
123
+ Args:
124
+ data: KV cache vectors in PCA space, shape (n_samples, head_dim)
125
+ head_dim: Dimension of each vector
126
+ max_bits: Maximum bits to calibrate
127
+
128
+ Returns:
129
+ Dictionary mapping bits -> distortion factor (relative to unquantized)
130
+ """
131
+ import torch
132
+ import torch.nn.functional as F
133
+ from nexusquant.core.e8_lattice import E8Lattice
134
+ from nexusquant.core.hadamard import hadamard_matrix
135
+
136
+ if isinstance(data, np.ndarray):
137
+ data = torch.from_numpy(data).float()
138
+
139
+ total_var = data.var().item()
140
+ if total_var < 1e-10:
141
+ return DISTORTION_THEORETICAL.copy()
142
+
143
+ factors = {0: 1.0}
144
+ for bits in range(1, max_bits + 1):
145
+ levels = 2 ** bits
146
+ pad = (8 - head_dim % 8) % 8
147
+ padded = F.pad(data, (0, pad)) if pad > 0 else data
148
+
149
+ p2 = 1
150
+ while p2 < padded.shape[-1]:
151
+ p2 *= 2
152
+ H = hadamard_matrix(p2)
153
+ padded = F.pad(padded, (0, p2 - padded.shape[-1]))
154
+ rotated = padded @ H.T
155
+ quantized = E8Lattice.quantize(rotated, levels=levels)
156
+ reconstructed = (quantized @ H)[:, :head_dim]
157
+
158
+ mse = ((data - reconstructed) ** 2).mean().item()
159
+ factors[bits] = mse / total_var
160
+
161
+ return factors