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 +32 -0
- nexusquant/core/__init__.py +40 -0
- nexusquant/core/compression_accounting.py +372 -0
- nexusquant/core/dp_allocator.py +161 -0
- nexusquant/core/e8_lattice.py +124 -0
- nexusquant/core/entropy_coder.py +615 -0
- nexusquant/core/hadamard.py +87 -0
- nexusquant/core/mp_threshold.py +79 -0
- nexusquant/core/nsn.py +233 -0
- nexusquant/core/optimal_shrinkage.py +258 -0
- nexusquant/core/rope_utils.py +109 -0
- nexusquant/core/tcc.py +470 -0
- nexusquant/core/temporal_codec.py +157 -0
- nexusquant/core/token_merger.py +123 -0
- nexusquant/integrations/__init__.py +1 -0
- nexusquant/integrations/huggingface.py +617 -0
- nexusquant/pipeline.py +651 -0
- nexusquant_kv-0.4.0.dist-info/METADATA +142 -0
- nexusquant_kv-0.4.0.dist-info/RECORD +22 -0
- nexusquant_kv-0.4.0.dist-info/WHEEL +5 -0
- nexusquant_kv-0.4.0.dist-info/licenses/LICENSE +98 -0
- nexusquant_kv-0.4.0.dist-info/top_level.txt +1 -0
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
|