Repository navigation
Expand file tree
/
Copy pathrans_codec.py
More file actions
287 lines (222 loc) · 9.35 KB
/
Copy pathrans_codec.py
File metadata and controls
287 lines (222 loc) · 9.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
#!/usr/bin/env python3
"""
rANS (Range Asymmetric Numeral Systems) Codec for LLM Weight Compression
This is a production-quality implementation optimized for:
1. 4-bit quantized weight indices (16 symbols)
2. Tile-parallel encoding/decoding
3. Benchmarking decode throughput
Based on Fabian Giesen's rANS implementation.
"""
import numpy as np
from dataclasses import dataclass
from typing import Tuple, List
import time
# Constants
PROB_BITS = 12 # Probability precision (4096 total)
PROB_SCALE = 1 << PROB_BITS
RANS_L = 1 << 23 # Lower bound for renormalization
RANS_B = 1 << 8 # Byte size
@dataclass
class RANSTable:
"""Precomputed tables for fast rANS encoding/decoding."""
freq: np.ndarray # Symbol frequencies (scaled to PROB_SCALE)
cumfreq: np.ndarray # Cumulative frequencies
sym_table: np.ndarray # Lookup table: slot -> symbol
@classmethod
def from_histogram(cls, counts: np.ndarray) -> 'RANSTable':
"""Build rANS tables from symbol counts."""
n_syms = len(counts)
# Normalize frequencies to sum to PROB_SCALE
counts = np.maximum(counts, 1).astype(np.float64) # Avoid zero freq
freq = (counts / counts.sum() * PROB_SCALE).astype(np.int32)
# Ensure sum equals PROB_SCALE
diff = PROB_SCALE - freq.sum()
freq[np.argmax(freq)] += diff
# Cumulative frequencies
cumfreq = np.zeros(n_syms + 1, dtype=np.int32)
cumfreq[1:] = np.cumsum(freq)
# Build symbol lookup table
sym_table = np.zeros(PROB_SCALE, dtype=np.uint8)
for s in range(n_syms):
sym_table[cumfreq[s]:cumfreq[s+1]] = s
return cls(freq=freq, cumfreq=cumfreq, sym_table=sym_table)
def rans_encode_tile(symbols: np.ndarray, table: RANSTable) -> bytes:
"""
Encode a tile of symbols using rANS.
rANS encodes in reverse order (LIFO), so we process symbols backwards.
"""
symbols = symbols.flatten().astype(np.int32)
n = len(symbols)
# Pre-allocate output buffer (worst case: 2 bytes per symbol)
output = np.zeros(n * 2 + 8, dtype=np.uint8)
out_ptr = 0
# Initial state
state = RANS_L
# Encode in reverse order
for i in range(n - 1, -1, -1):
s = symbols[i]
freq_s = int(table.freq[s])
cumfreq_s = int(table.cumfreq[s])
# Renormalize: output bytes while state is too large
max_state = RANS_L * freq_s
while state >= max_state:
output[out_ptr] = state & 0xFF
out_ptr += 1
state >>= 8
# Core rANS encoding step
state = (state // freq_s) * PROB_SCALE + cumfreq_s + (state % freq_s)
# Flush final state (4 bytes, big-endian for decode order)
for _ in range(4):
output[out_ptr] = state & 0xFF
out_ptr += 1
state >>= 8
# Return bytes in decode order (reversed)
return bytes(output[:out_ptr][::-1])
def rans_decode_tile(data: bytes, n_symbols: int, table: RANSTable) -> np.ndarray:
"""
Decode a tile of symbols from rANS-encoded data.
This is the hot path - needs to be fast!
"""
data = np.frombuffer(data, dtype=np.uint8)
ptr = 0
# Initialize state from first 4 bytes
state = 0
for _ in range(4):
state = (state << 8) | int(data[ptr])
ptr += 1
# Decode symbols
output = np.zeros(n_symbols, dtype=np.uint8)
mask = PROB_SCALE - 1
for i in range(n_symbols):
# Extract slot from state
slot = state & mask
# Lookup symbol
s = table.sym_table[slot]
output[i] = s
# Update state
freq_s = int(table.freq[s])
cumfreq_s = int(table.cumfreq[s])
state = freq_s * (state >> PROB_BITS) + slot - cumfreq_s
# Renormalize: read bytes while state is too small
while state < RANS_L and ptr < len(data):
state = (state << 8) | int(data[ptr])
ptr += 1
return output
def benchmark_codec(n_elements: int = 1_000_000, n_trials: int = 5) -> dict:
"""Benchmark encode/decode throughput."""
# Generate realistic quantized weight distribution (near-Gaussian)
np.random.seed(42)
weights = np.random.randn(n_elements).astype(np.float32) * 0.02
# Quantize to 4-bit
w_min, w_max = weights.min(), weights.max()
scale = (w_max - w_min) / 15
indices = np.clip(np.round((weights - w_min) / scale), 0, 15).astype(np.uint8)
# Build frequency table
counts = np.bincount(indices, minlength=16)
table = RANSTable.from_histogram(counts)
# Calculate entropy
probs = counts / counts.sum()
probs = probs[probs > 0]
entropy = -np.sum(probs * np.log2(probs))
print(f"\n{'='*60}")
print(f" rANS Codec Benchmark ({n_elements:,} elements)")
print(f"{'='*60}")
print(f"Entropy: {entropy:.3f} bits/symbol")
print(f"Theoretical compression: {4.0/entropy:.2f}x over 4-bit")
# Benchmark encoding
encode_times = []
for _ in range(n_trials):
t0 = time.perf_counter()
compressed = rans_encode_tile(indices, table)
encode_times.append(time.perf_counter() - t0)
avg_encode = np.mean(encode_times)
compressed_size = len(compressed)
original_size = (n_elements + 1) // 2 # 4-bit packed
compression_ratio = original_size / compressed_size
bits_per_symbol = (compressed_size * 8) / n_elements
print(f"\nEncoding:")
print(f" Original (4-bit packed): {original_size:,} bytes")
print(f" Compressed (rANS): {compressed_size:,} bytes")
print(f" Compression ratio: {compression_ratio:.2f}x over 4-bit")
print(f" Bits per symbol: {bits_per_symbol:.3f}")
print(f" Encode time: {avg_encode*1000:.2f} ms")
print(f" Encode throughput: {n_elements/avg_encode/1e6:.1f} M symbols/s")
# Benchmark decoding
decode_times = []
for _ in range(n_trials):
t0 = time.perf_counter()
decoded = rans_decode_tile(compressed, n_elements, table)
decode_times.append(time.perf_counter() - t0)
avg_decode = np.mean(decode_times)
# Verify lossless
is_correct = np.array_equal(indices, decoded)
print(f"\nDecoding:")
print(f" Decode time: {avg_decode*1000:.2f} ms")
print(f" Decode throughput: {n_elements/avg_decode/1e6:.1f} M symbols/s")
print(f" Lossless: {is_correct}")
# Estimate GPU decode potential
# GPU can do ~1000x more ops/sec than Python
estimated_gpu_decode = avg_decode / 1000
print(f"\nEstimated Metal decode:")
print(f" Time (est): {estimated_gpu_decode*1000:.4f} ms")
print(f" Throughput (est): {n_elements/estimated_gpu_decode/1e9:.1f} G symbols/s")
return {
"entropy": entropy,
"compression_ratio": compression_ratio,
"bits_per_symbol": bits_per_symbol,
"encode_throughput_M": n_elements / avg_encode / 1e6,
"decode_throughput_M": n_elements / avg_decode / 1e6,
"is_lossless": is_correct,
}
def test_on_real_weights():
"""Test codec on real model weights."""
from safetensors import safe_open
from pathlib import Path
model_path = Path("models/openai-community_gpt2/model.safetensors")
if not model_path.exists():
print("GPT-2 model not found, skipping real weight test")
return
print(f"\n{'='*60}")
print(f" Testing on Real GPT-2 Weights")
print(f"{'='*60}")
total_original = 0
total_compressed = 0
total_params = 0
with safe_open(model_path, framework="numpy") as f:
for name in list(f.keys())[:10]: # First 10 tensors
weights = f.get_tensor(name)
if weights.ndim < 2 or weights.size < 10000:
continue
# Quantize
w_min, w_max = weights.min(), weights.max()
scale = (w_max - w_min) / 15
if scale == 0:
continue
indices = np.clip(np.round((weights - w_min) / scale), 0, 15).astype(np.uint8)
# Build table and encode
counts = np.bincount(indices.flatten(), minlength=16)
table = RANSTable.from_histogram(counts)
t0 = time.perf_counter()
compressed = rans_encode_tile(indices.flatten(), table)
encode_time = time.perf_counter() - t0
# Stats
original_bytes = (indices.size + 1) // 2
compressed_bytes = len(compressed)
ratio = original_bytes / compressed_bytes
total_original += original_bytes
total_compressed += compressed_bytes
total_params += indices.size
print(f"{name[:40]:<40} {weights.shape} → {ratio:.2f}x")
if total_compressed > 0:
overall_ratio = total_original / total_compressed
print(f"\n{'='*60}")
print(f"Overall compression: {overall_ratio:.2f}x over 4-bit")
print(f"Total params: {total_params:,}")
print(f"4-bit size: {total_original/1024:.1f} KB")
print(f"rANS size: {total_compressed/1024:.1f} KB")
if __name__ == "__main__":
# Run benchmarks
benchmark_codec(1_000_000)
benchmark_codec(10_000_000)
# Test on real weights
test_on_real_weights()