toxzak commited on
Commit
9c41926
·
verified ·
1 Parent(s): 2c9dbfc

Initial upload: mixed-budget sub-4-bit artifacts + perplexity result

Browse files
README.md ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # sub1quant — sub-4-bit quantization artifacts for gemma-4-E2B
2
+
3
+ This repo holds the **mixed-budget sub-4-bit quantization artifacts** produced by the
4
+ `sub1quant` project. The base model (`google/gemma-4-E2B`) is **not** mirrored here —
5
+ pull it separately from Hugging Face.
6
+
7
+ ## Contents
8
+
9
+ ```
10
+ quantized/
11
+ gemma_mixed_budget_full_g128_target4p0.pt # 948 MB, 316 language_model weight tensors
12
+ # 301 groupwise_int4 + 14 int2_binary_residual
13
+ # + 1 int2_error_budget_residual
14
+ # avg BPW ≈ 4.0 (vs BF16 ≈ 16 BPW)
15
+ eval_results/
16
+ mixed_budget_full_g128_target4p0_ppl_colab.json # perplexity on wikitext test
17
+ mixed_budget_scan_full_g128_target4p0.json # full-surface reconstruction scan
18
+ error_budget_residual_*.json # earlier int2+residual scan results
19
+ src/ # quantization/dequant primitives
20
+ scripts/ # quantize_mixed_budget, eval_quantized, ...
21
+ test_perplexity.py # entry-point for perplexity eval
22
+ data/wiki.test.txt # wikitext-103 test, ~287k tokens
23
+ ```
24
+
25
+ ## Perplexity (latest)
26
+
27
+ | format | BPW | perplexity | chunks | tokens | status |
28
+ |--------|----:|-----------:|-------:|-------:|--------|
29
+ | mixed_budget_full_g128_target4p0 | 4.00 | **107.2452** | 571 | 292282 | FAIL (>10.5) |
30
+
31
+ Run on `NVIDIA L4` bf16→fp16, `gemma-4-E2B` from `google/gemma-4-E2B`, wikitext test
32
+ (stride=512, max_length=512). Result file:
33
+ `eval_results/mixed_budget_full_g128_target4p0_ppl_colab.json`.
34
+
35
+ The 107 perplexity is materially higher than a working sub-4-bit quant on a 2B
36
+ model — treat it as a measurement, not a quality claim. Reconstruction RMSE alone
37
+ (see scan JSONs) does not predict this number.
38
+
39
+ ## Reproducing the perplexity eval
40
+
41
+ ```bash
42
+ # install
43
+ pip install "transformers>=5.5.0" torch accelerate safetensors
44
+
45
+ # pull the base model (NOT in this repo)
46
+ python -c "from huggingface_hub import snapshot_download; snapshot_download('google/gemma-4-E2B', local_dir='./models/gemma-4-E2B')"
47
+
48
+ # run
49
+ python test_perplexity.py \
50
+ --model models/gemma-4-E2B \
51
+ --quantized quantized/gemma_mixed_budget_full_g128_target4p0.pt \
52
+ --wikitext data/wiki.test.txt \
53
+ --device cuda \
54
+ --max-length 512 --stride 512
55
+ ```
56
+
57
+ ## License
58
+
59
+ The base model is governed by Google's Gemma license. The quantization
60
+ artifacts in this repo are released under Apache-2.0.
data/wiki.test.txt ADDED
The diff for this file is too large to render. See raw diff
 
eval_results/mixed_budget_full_g128_target4p0_ppl_colab.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "timestamp_utc": 1782706594,
3
+ "method": "mixed_budget",
4
+ "group_size": 128,
5
+ "target_bpw": 4.0,
6
+ "quantized_pt": "/content/sub1quant/quantized/gemma_mixed_budget_full_g128_target4p0.pt",
7
+ "model_dir": "/content/sub1quant/models/gemma-4-E2B",
8
+ "wikitext": "/content/sub1quant/data/wiki.test.txt",
9
+ "device": "cuda",
10
+ "max_length": 512,
11
+ "stride": 512,
12
+ "seq_len_tokens": 292282,
13
+ "n_chunks": 571,
14
+ "weights_replaced": 276,
15
+ "weights_total_pt": 316,
16
+ "weights_skipped_shared_kv": 40,
17
+ "perplexity": 107.2452,
18
+ "target_ppl": 10.5,
19
+ "status": "FAIL"
20
+ }
quantized/gemma_mixed_budget_full_g128_target4p0.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c4337f598433a909e3895a3d3b47f2093dbdba2a1ed4e71502f5549e233f6326
3
+ size 948181931
scripts/eval_quantized.py ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import gc
3
+ import argparse
4
+ import json
5
+ import struct
6
+ from pathlib import Path
7
+ import sys
8
+ from typing import Callable, Iterable, Optional
9
+
10
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
11
+ from src.error_budget_residual import dequantize_binary_residual, dequantize_error_budget_residual
12
+ from src.groupwise_int4 import dequantize_groupwise_int4
13
+ from src.quantization import ternary_unpack, sigma_dequantize
14
+
15
+
16
+ def _as_tensor(value, device: str, dtype=torch.float32) -> torch.Tensor:
17
+ if isinstance(value, torch.Tensor):
18
+ return value.to(device=device, dtype=dtype)
19
+ return torch.tensor(value, device=device, dtype=dtype)
20
+
21
+
22
+ def _shape_tuple(shape) -> Optional[tuple]:
23
+ if shape is None:
24
+ return None
25
+ if isinstance(shape, torch.Tensor):
26
+ shape = shape.tolist()
27
+ return tuple(int(dim) for dim in shape)
28
+
29
+
30
+ def quantized_entry_shape(q_entry: dict) -> Optional[tuple]:
31
+ shape = q_entry.get('original_shape', q_entry.get('orig_shape'))
32
+ if shape is not None:
33
+ return _shape_tuple(shape)
34
+ if 'U_shape' in q_entry and 'Vt_shape' in q_entry:
35
+ u_shape = _shape_tuple(q_entry['U_shape'])
36
+ vt_shape = _shape_tuple(q_entry['Vt_shape'])
37
+ return (u_shape[0], vt_shape[1])
38
+ return None
39
+
40
+
41
+ def normalize_checkpoint_weight_keys(weight_keys: Optional[Iterable]) -> list[tuple[str, tuple]]:
42
+ if not weight_keys:
43
+ return []
44
+
45
+ normalized = []
46
+ for item in weight_keys:
47
+ if isinstance(item, dict):
48
+ key = item.get('key') or item.get('name')
49
+ shape = item.get('shape') or item.get('original_shape') or item.get('orig_shape')
50
+ else:
51
+ key, shape = item
52
+ if key:
53
+ normalized.append((key, _shape_tuple(shape)))
54
+ return normalized
55
+
56
+
57
+ def load_model_weight_keys(model_dir: str | Path) -> list[tuple[str, tuple]]:
58
+ """Recover quantization key order from local safetensors metadata."""
59
+ model_dir = Path(model_dir)
60
+ safetensor_files = sorted(model_dir.glob("*.safetensors"))
61
+ weight_keys = []
62
+
63
+ for safetensor_path in safetensor_files:
64
+ with open(safetensor_path, 'rb') as f:
65
+ header_size = struct.unpack('<Q', f.read(8))[0]
66
+ header = json.loads(f.read(header_size))
67
+
68
+ for key, info in header.items():
69
+ if key == '__metadata__' or not isinstance(info, dict):
70
+ continue
71
+ shape = info.get('shape')
72
+ if 'weight' not in key or shape is None or len(shape) != 2:
73
+ continue
74
+ if any(x in key for x in [
75
+ 'lm_head',
76
+ 'embed_tokens',
77
+ 'norm',
78
+ 'audio_tower',
79
+ 'vision_tower',
80
+ 'embed_vision',
81
+ ]):
82
+ continue
83
+ weight_keys.append((key, _shape_tuple(shape)))
84
+
85
+ language_model_keys = [item for item in weight_keys if 'language_model' in item[0]]
86
+ return language_model_keys or weight_keys
87
+
88
+
89
+ def resolve_quantized_weight_key(
90
+ entry_index,
91
+ q_entry: dict,
92
+ fallback_weight_keys: Optional[list[tuple[str, tuple]]] = None,
93
+ ) -> str:
94
+ key = q_entry.get('key')
95
+ if key:
96
+ return key
97
+
98
+ if fallback_weight_keys:
99
+ try:
100
+ fallback_index = int(entry_index)
101
+ except (TypeError, ValueError) as exc:
102
+ raise KeyError(
103
+ f"Quantized entry {entry_index!r} has no key and cannot be mapped by index"
104
+ ) from exc
105
+
106
+ if fallback_index >= len(fallback_weight_keys):
107
+ raise KeyError(
108
+ f"Quantized entry {entry_index!r} has no key and exceeds "
109
+ f"{len(fallback_weight_keys)} fallback model weights"
110
+ )
111
+
112
+ key, expected_shape = fallback_weight_keys[fallback_index]
113
+ entry_shape = quantized_entry_shape(q_entry)
114
+ if entry_shape is not None and expected_shape is not None and entry_shape != expected_shape:
115
+ raise ValueError(
116
+ f"Legacy quantized entry {entry_index!r} maps to {key}, but "
117
+ f"checkpoint shape {entry_shape} != model shape {expected_shape}"
118
+ )
119
+ return key
120
+
121
+ raise KeyError(
122
+ f"Quantized entry {entry_index!r} has no source key. Pass a local "
123
+ "model directory with safetensors metadata or use a checkpoint that stores keys."
124
+ )
125
+
126
+
127
+ def reconstruct_weight(q_entry: dict, device: str = 'cpu') -> torch.Tensor:
128
+ U_scale = _as_tensor(q_entry['U_scale'], device)
129
+ Vt_scale = _as_tensor(q_entry['Vt_scale'], device)
130
+ S_scale = _as_tensor(q_entry['S_scale'], device)
131
+ U = ternary_unpack(q_entry['U_packed'].to(device), q_entry['U_shape']).float() * U_scale
132
+ Vt = ternary_unpack(q_entry['Vt_packed'].to(device), q_entry['Vt_shape']).float() * Vt_scale
133
+ S = sigma_dequantize(q_entry['S'].to(device), S_scale)
134
+ return torch.matmul(U * S.unsqueeze(0), Vt)
135
+
136
+
137
+ def reconstruct_quantized_entry(q_entry: dict, device: str = 'cpu') -> torch.Tensor:
138
+ if q_entry.get('format') == 'groupwise_int4' or 'packed_int4' in q_entry:
139
+ return dequantize_groupwise_int4(q_entry, device)
140
+
141
+ if q_entry.get('format') == 'int2_error_budget_residual':
142
+ return dequantize_error_budget_residual(q_entry, device)
143
+
144
+ if q_entry.get('format') == 'int2_base':
145
+ return dequantize_binary_residual(q_entry, device, include_residual=False)
146
+
147
+ if q_entry.get('format') == 'int2_binary_residual' or 'base_packed' in q_entry:
148
+ return dequantize_binary_residual(q_entry, device)
149
+
150
+ if {'U_packed', 'Vt_packed', 'S'}.issubset(q_entry):
151
+ return reconstruct_weight(q_entry, device)
152
+
153
+ if 'packed' in q_entry:
154
+ shape = q_entry['orig_shape']
155
+ scale = _as_tensor(q_entry['scale'], device)
156
+ return ternary_unpack(q_entry['packed'].to(device), shape).float() * scale
157
+
158
+ if 'q' in q_entry:
159
+ num_bits = int(q_entry.get('num_bits', 0))
160
+ qmax = 2 ** (num_bits - 1) - 1
161
+ if qmax <= 0:
162
+ raise ValueError(f"Unsupported num_bits for magnitude checkpoint: {num_bits}")
163
+
164
+ q = q_entry['q'].to(device).float()
165
+ shape = quantized_entry_shape(q_entry)
166
+ if shape is not None and q.numel() == shape[0] * shape[1]:
167
+ q = q.reshape(shape)
168
+
169
+ scale = q_entry['scale']
170
+ if q_entry.get('per_channel'):
171
+ scale = _as_tensor(scale, device).reshape(-1, 1)
172
+ else:
173
+ scale = _as_tensor(scale, device)
174
+ return q * scale / qmax
175
+
176
+ raise ValueError(f"Unknown quantized entry format: {sorted(q_entry.keys())}")
177
+
178
+
179
+ def build_model_weight_map(model) -> dict[str, object]:
180
+ weight_map = {}
181
+ for name, module in model.named_modules():
182
+ if getattr(module, 'weight', None) is None:
183
+ continue
184
+ weight_key = f"{name}.weight" if name else "weight"
185
+ weight_map[weight_key] = module
186
+ return weight_map
187
+
188
+
189
+ def is_expected_missing_shared_kv_weight(key: str, weight_map: dict[str, object]) -> bool:
190
+ if key.endswith('.self_attn.k_proj.weight'):
191
+ q_key = key.replace('.k_proj.weight', '.q_proj.weight')
192
+ o_key = key.replace('.k_proj.weight', '.o_proj.weight')
193
+ return q_key in weight_map or o_key in weight_map
194
+ if key.endswith('.self_attn.v_proj.weight'):
195
+ q_key = key.replace('.v_proj.weight', '.q_proj.weight')
196
+ o_key = key.replace('.v_proj.weight', '.o_proj.weight')
197
+ return q_key in weight_map or o_key in weight_map
198
+ return False
199
+
200
+
201
+ def apply_quantized_weights(
202
+ model,
203
+ quantized: dict,
204
+ device: str = 'cpu',
205
+ model_dir: str | Path | None = None,
206
+ checkpoint_weight_keys: Optional[Iterable] = None,
207
+ reconstruct_fn: Callable[[dict, str], torch.Tensor] = reconstruct_quantized_entry,
208
+ strict: bool = True,
209
+ ) -> dict:
210
+ fallback_weight_keys = normalize_checkpoint_weight_keys(checkpoint_weight_keys)
211
+ if not fallback_weight_keys and model_dir is not None:
212
+ fallback_weight_keys = load_model_weight_keys(model_dir)
213
+
214
+ weight_map = build_model_weight_map(model)
215
+ stats = {'replaced': 0, 'skipped': [], 'missing': [], 'shape_mismatches': []}
216
+
217
+ for entry_index, q_entry in quantized.items():
218
+ key = resolve_quantized_weight_key(entry_index, q_entry, fallback_weight_keys)
219
+ module = weight_map.get(key)
220
+ if module is None:
221
+ if is_expected_missing_shared_kv_weight(key, weight_map):
222
+ stats['skipped'].append(key)
223
+ continue
224
+ message = f"No model module found for quantized weight {key}"
225
+ if strict:
226
+ raise KeyError(message)
227
+ stats['missing'].append(message)
228
+ continue
229
+
230
+ reconstructed = reconstruct_fn(q_entry, device)
231
+ target_shape = tuple(module.weight.shape)
232
+ if tuple(reconstructed.shape) != target_shape:
233
+ message = (
234
+ f"Shape mismatch for {key}: reconstructed {tuple(reconstructed.shape)} "
235
+ f"!= model {target_shape}"
236
+ )
237
+ if strict:
238
+ raise ValueError(message)
239
+ stats['shape_mismatches'].append(message)
240
+ continue
241
+
242
+ with torch.no_grad():
243
+ module.weight.data = reconstructed.to(
244
+ dtype=module.weight.dtype,
245
+ device=module.weight.device,
246
+ )
247
+ stats['replaced'] += 1
248
+
249
+ return stats
250
+
251
+
252
+ def eval_perplexity(model, tokenizer, wikitext_path: str, device: str,
253
+ max_length: int = 512, stride: int = 512):
254
+ with open(wikitext_path, 'r', encoding='utf-8') as f:
255
+ text = f.read()
256
+
257
+ print("Tokenizing...")
258
+ encodings = tokenizer(text, return_tensors='pt')
259
+ encodings = {k: v.to(device) for k, v in encodings.items()}
260
+ seq_len = encodings['input_ids'].shape[1]
261
+ print(f" Sequence length: {seq_len} tokens")
262
+
263
+ nlls = []
264
+ prev_end_loc = 0
265
+
266
+ for begin_loc in range(0, seq_len, stride):
267
+ end_loc = min(begin_loc + max_length, seq_len)
268
+ trg_len = end_loc - prev_end_loc
269
+
270
+ input_ids = encodings['input_ids'][:, begin_loc:end_loc]
271
+ target_ids = input_ids.clone()
272
+ target_ids[:, :-trg_len] = -100
273
+
274
+ with torch.no_grad():
275
+ outputs = model(input_ids, labels=target_ids)
276
+ neg_log_likelihood = outputs.loss * trg_len
277
+
278
+ nlls.append(neg_log_likelihood)
279
+ prev_end_loc = end_loc
280
+
281
+ if end_loc >= seq_len:
282
+ break
283
+
284
+ avg_nll = torch.stack(nlls).sum() / seq_len
285
+ perplexity = torch.exp(avg_nll).item()
286
+ return perplexity, {'n_chunks': len(nlls), 'seq_len': seq_len}
287
+
288
+
289
+ def main():
290
+ parser = argparse.ArgumentParser(description="Evaluate quantized model perplexity")
291
+ parser.add_argument('--quantized-pt', default='quantized/gemma-4-E2B-sub1bit.pt',
292
+ help='Path to quantized .pt checkpoint')
293
+ parser.add_argument('--model-dir', default='models/gemma-4-E2B',
294
+ help='Path to base model directory')
295
+ parser.add_argument('--wikitext', default='data/wiki.test.txt',
296
+ help='Path to WikiText test file')
297
+ parser.add_argument('--device', default=None,
298
+ help='Device (auto-detect if not set)')
299
+ parser.add_argument('--max-length', type=int, default=512)
300
+ parser.add_argument('--stride', type=int, default=512)
301
+ args = parser.parse_args()
302
+
303
+ if args.device:
304
+ device = args.device
305
+ else:
306
+ device = "cuda" if torch.cuda.is_available() else "cpu"
307
+
308
+ wikitext_path = Path(args.wikitext)
309
+ if not wikitext_path.exists():
310
+ print(f"WikiText not found: {wikitext_path}")
311
+ return
312
+
313
+ from transformers import AutoModelForCausalLM, AutoTokenizer
314
+
315
+ print("=" * 60)
316
+ print("QUANTIZED MODEL PERPLEXITY EVALUATION")
317
+ print("=" * 60)
318
+ print(f"Device: {device}")
319
+ print(f"Quantized: {args.quantized_pt}")
320
+ print(f"Base model: {args.model_dir}")
321
+ print(f"WikiText: {args.wikitext}")
322
+ print()
323
+
324
+ # 1. Load quantized checkpoint
325
+ print("[1] Loading quantized checkpoint...")
326
+ q_data = torch.load(args.quantized_pt, map_location='cpu', weights_only=True)
327
+ quantized = q_data['quantized']
328
+ print(f" {len(quantized)} quantized entries")
329
+ print()
330
+
331
+ # 2. Load base model
332
+ print("[2] Loading base model...")
333
+ torch_dtype = torch.float16 if device == "cuda" else torch.float32
334
+ tokenizer = AutoTokenizer.from_pretrained(args.model_dir, trust_remote_code=True)
335
+ model = AutoModelForCausalLM.from_pretrained(
336
+ args.model_dir,
337
+ device_map=device,
338
+ torch_dtype=torch_dtype,
339
+ trust_remote_code=True
340
+ )
341
+ model.eval()
342
+ print()
343
+
344
+ # 3. Reconstruct weights
345
+ print("[3] Applying quantized weights...")
346
+ apply_stats = apply_quantized_weights(
347
+ model,
348
+ quantized,
349
+ device=device,
350
+ model_dir=args.model_dir,
351
+ checkpoint_weight_keys=q_data.get('weight_keys'),
352
+ )
353
+ print(f" Replaced {apply_stats['replaced']}/{len(quantized)} weights")
354
+ if apply_stats['skipped']:
355
+ print(f" Skipped {len(apply_stats['skipped'])} shared-KV checkpoint entries")
356
+ print()
357
+
358
+ # 4. Evaluate perplexity
359
+ print("[4] Evaluating perplexity...")
360
+ ppl, stats = eval_perplexity(
361
+ model, tokenizer, str(wikitext_path), device,
362
+ max_length=args.max_length, stride=args.stride
363
+ )
364
+ print()
365
+
366
+ print("=" * 60)
367
+ print("RESULTS")
368
+ print("=" * 60)
369
+ print(f" Perplexity: {ppl:.4f}")
370
+ print(f" Chunks: {stats['n_chunks']}")
371
+ print(f" Target: <= 10.5")
372
+ status = "PASS" if ppl <= 10.5 else "FAIL"
373
+ print(f" Status: {status}")
374
+ print("=" * 60)
375
+
376
+ del model
377
+ gc.collect()
378
+ if device == "cuda":
379
+ torch.cuda.empty_cache()
380
+
381
+
382
+ if __name__ == "__main__":
383
+ main()
scripts/limited_ppl_bench.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run a short WikiText perplexity smoke benchmark for base/quantized checkpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import gc
7
+ import json
8
+ import sys
9
+ import time
10
+ from pathlib import Path
11
+
12
+ import torch
13
+
14
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
15
+ from scripts.eval_quantized import apply_quantized_weights
16
+
17
+
18
+ def eval_limited_ppl(
19
+ model,
20
+ tokenizer,
21
+ text: str,
22
+ device: str,
23
+ tokens: int,
24
+ max_length: int,
25
+ stride: int,
26
+ ) -> dict:
27
+ encodings = tokenizer(text, return_tensors="pt")
28
+ input_ids = encodings["input_ids"][:, :tokens].to(device)
29
+ seq_len = input_ids.shape[1]
30
+ nlls = []
31
+ prev_end_loc = 0
32
+
33
+ for begin_loc in range(0, seq_len, stride):
34
+ end_loc = min(begin_loc + max_length, seq_len)
35
+ trg_len = end_loc - prev_end_loc
36
+
37
+ batch = input_ids[:, begin_loc:end_loc]
38
+ target = batch.clone()
39
+ target[:, :-trg_len] = -100
40
+
41
+ with torch.no_grad():
42
+ outputs = model(batch, labels=target)
43
+ nlls.append(outputs.loss.detach() * trg_len)
44
+
45
+ prev_end_loc = end_loc
46
+ if end_loc >= seq_len:
47
+ break
48
+
49
+ ppl = torch.exp(torch.stack(nlls).sum() / seq_len).item()
50
+ return {"ppl": ppl, "seq_len": seq_len, "chunks": len(nlls)}
51
+
52
+
53
+ def run(args: argparse.Namespace) -> dict:
54
+ from transformers import AutoModelForCausalLM, AutoTokenizer
55
+
56
+ model_dir = Path(args.model_dir)
57
+ device = args.device or ("cuda" if torch.cuda.is_available() else "cpu")
58
+ dtype = torch.bfloat16 if device == "cuda" else torch.float32
59
+
60
+ tokenizer = AutoTokenizer.from_pretrained(str(model_dir), trust_remote_code=True)
61
+ model = AutoModelForCausalLM.from_pretrained(
62
+ str(model_dir),
63
+ dtype=dtype,
64
+ device_map=device,
65
+ trust_remote_code=True,
66
+ )
67
+ model.eval()
68
+
69
+ apply_stats = {"replaced": 0, "skipped": []}
70
+ checkpoint_stats = None
71
+ if args.quantized_pt:
72
+ q_data = torch.load(args.quantized_pt, map_location="cpu", weights_only=True)
73
+ checkpoint_stats = q_data.get("stats")
74
+ apply_stats = apply_quantized_weights(
75
+ model,
76
+ q_data["quantized"],
77
+ device=device,
78
+ model_dir=model_dir,
79
+ checkpoint_weight_keys=q_data.get("weight_keys"),
80
+ strict=False,
81
+ )
82
+ del q_data
83
+ gc.collect()
84
+
85
+ text = Path(args.wikitext).read_text(encoding="utf-8")
86
+ metrics = eval_limited_ppl(
87
+ model,
88
+ tokenizer,
89
+ text,
90
+ device,
91
+ tokens=args.tokens,
92
+ max_length=args.max_length,
93
+ stride=args.stride,
94
+ )
95
+ metrics.update(
96
+ {
97
+ "label": args.label,
98
+ "mode": "quantized" if args.quantized_pt else "base",
99
+ "quantized_pt": args.quantized_pt,
100
+ "apply_stats": apply_stats,
101
+ "checkpoint_stats": checkpoint_stats,
102
+ "device": device,
103
+ }
104
+ )
105
+ return metrics
106
+
107
+
108
+ def parse_args() -> argparse.Namespace:
109
+ parser = argparse.ArgumentParser()
110
+ parser.add_argument("--label", required=True)
111
+ parser.add_argument("--model-dir", default="models/gemma-4-E2B")
112
+ parser.add_argument("--wikitext", default="data/wiki.test.txt")
113
+ parser.add_argument("--quantized-pt", default=None)
114
+ parser.add_argument("--tokens", type=int, default=4096)
115
+ parser.add_argument("--max-length", type=int, default=512)
116
+ parser.add_argument("--stride", type=int, default=512)
117
+ parser.add_argument("--device", default=None)
118
+ parser.add_argument("--output", required=True)
119
+ return parser.parse_args()
120
+
121
+
122
+ def main() -> None:
123
+ args = parse_args()
124
+ start = time.time()
125
+ result = run(args)
126
+ result["elapsed_s"] = round(time.time() - start, 1)
127
+ output = Path(args.output)
128
+ output.parent.mkdir(parents=True, exist_ok=True)
129
+ output.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
130
+ print("RESULT=" + json.dumps(result, indent=2), flush=True)
131
+
132
+
133
+ if __name__ == "__main__":
134
+ main()
scripts/quantize_mixed_budget.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build a mixed-budget checkpoint from a scan_mixed_budget allocation JSON."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import gc
7
+ import json
8
+ import re
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import torch
13
+
14
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
15
+ from scripts.eval_quantized import reconstruct_quantized_entry
16
+ from scripts.quantize_groupwise_int4 import iter_model_weights
17
+ from src.error_budget_residual import estimate_binary_residual_bpw, quantize_binary_residual, quantize_error_budget_residual
18
+ from src.groupwise_int4 import quantize_groupwise_int4
19
+
20
+
21
+ ERROR_BUDGET_RE = re.compile(r"^int2_error_budget_k(\d+)$")
22
+
23
+
24
+ def build_selection_map(scan: dict) -> dict[str, str]:
25
+ selected = scan.get("mixed_allocation", {}).get("selected_layers", [])
26
+ if not selected:
27
+ raise ValueError("scan JSON does not contain mixed_allocation.selected_layers")
28
+ selection = {}
29
+ for item in selected:
30
+ key = item.get("key")
31
+ method = item.get("method")
32
+ if not key or not method:
33
+ raise ValueError(f"invalid selected layer entry: {item!r}")
34
+ selection[str(key)] = str(method)
35
+ return selection
36
+
37
+
38
+ def quantize_weight_for_method(weight: torch.Tensor, method: str, group_size: int) -> dict:
39
+ if method == "groupwise_int4":
40
+ return quantize_groupwise_int4(weight, group_size=group_size)
41
+ if method == "int2_binary_residual":
42
+ return quantize_binary_residual(weight, group_size=group_size)
43
+ if method == "int2_base":
44
+ entry = quantize_binary_residual(weight, group_size=group_size)
45
+ entry["format"] = "int2_base"
46
+ entry["bpw"] = estimate_binary_residual_bpw(weight.shape, group_size=group_size) - 1.0 - (16.0 / group_size)
47
+ return entry
48
+
49
+ match = ERROR_BUDGET_RE.match(method)
50
+ if match:
51
+ return quantize_error_budget_residual(
52
+ weight,
53
+ group_size=group_size,
54
+ outliers_per_group=int(match.group(1)),
55
+ )
56
+
57
+ raise ValueError(f"Unsupported mixed-budget method {method!r}")
58
+
59
+
60
+ def build_checkpoint(
61
+ scan: dict,
62
+ model_dir: Path,
63
+ group_size: int,
64
+ max_layers: int | None,
65
+ skip_reconstruction_metrics: bool,
66
+ ) -> dict:
67
+ selection = build_selection_map(scan)
68
+ quantized: dict[int, dict] = {}
69
+ layer_stats: list[dict] = []
70
+ total_params = 0
71
+ total_bits = 0.0
72
+ total_weighted_mse = 0.0
73
+
74
+ for idx, key, weight in iter_model_weights(model_dir, max_layers=max_layers):
75
+ method = selection.get(key)
76
+ if method is None:
77
+ raise KeyError(f"No mixed-budget selection for {key}")
78
+
79
+ entry = quantize_weight_for_method(weight, method, group_size=group_size)
80
+ entry["key"] = key
81
+ quantized[idx] = entry
82
+
83
+ params = weight.numel()
84
+ total_params += params
85
+ total_bits += float(entry["bpw"]) * params
86
+
87
+ mse = None
88
+ rmse = None
89
+ if not skip_reconstruction_metrics:
90
+ restored = reconstruct_quantized_entry(entry)
91
+ error = restored - weight
92
+ mse = error.pow(2).mean().item()
93
+ rmse = mse**0.5
94
+ total_weighted_mse += mse * params
95
+ del restored, error
96
+
97
+ layer_stats.append(
98
+ {
99
+ "idx": idx,
100
+ "key": key,
101
+ "method": method,
102
+ "shape": list(weight.shape),
103
+ "params": params,
104
+ "bpw": entry["bpw"],
105
+ "mse": mse,
106
+ "rmse": rmse,
107
+ }
108
+ )
109
+ print(
110
+ f"Layer {idx:3d}: {method}, bpw={entry['bpw']:.4f}, shape={list(weight.shape)}"
111
+ + ("" if mse is None else f", mse={mse:.6f}, rmse={rmse:.6f}"),
112
+ flush=True,
113
+ )
114
+
115
+ del weight
116
+ gc.collect()
117
+
118
+ if not quantized:
119
+ raise RuntimeError(f"No quantizable language_model 2D weights found in {model_dir}")
120
+
121
+ selected_keys = {item["key"] for item in layer_stats}
122
+ extra_keys = sorted(set(selection) - selected_keys)
123
+ if extra_keys:
124
+ raise KeyError(f"Scan allocation had selections not emitted by this build: {extra_keys[:5]}")
125
+
126
+ avg_bpw = total_bits / total_params
127
+ stats = {
128
+ "method": "mixed_budget",
129
+ "format_version": 1,
130
+ "group_size": group_size,
131
+ "layers": len(quantized),
132
+ "total_params": total_params,
133
+ "avg_bpw": avg_bpw,
134
+ "compression_vs_bf16": 16.0 / avg_bpw,
135
+ "weighted_mse": None if skip_reconstruction_metrics else total_weighted_mse / total_params,
136
+ "weighted_rmse": None if skip_reconstruction_metrics else (total_weighted_mse / total_params) ** 0.5,
137
+ "method_counts": _method_counts(layer_stats),
138
+ "layer_stats": layer_stats,
139
+ "source_allocation": scan.get("mixed_allocation"),
140
+ "uniform_int4": scan.get("uniform_int4"),
141
+ }
142
+ return {
143
+ "quantized": quantized,
144
+ "stats": stats,
145
+ "method": "mixed_budget",
146
+ "config": {
147
+ "model_dir": str(model_dir),
148
+ "group_size": group_size,
149
+ "max_layers": max_layers,
150
+ "source_scan": scan.get("source_scan"),
151
+ },
152
+ "weight_keys": [
153
+ {"key": item["key"], "shape": item["shape"], "method": item["method"]}
154
+ for item in layer_stats
155
+ ],
156
+ }
157
+
158
+
159
+ def _method_counts(layer_stats: list[dict]) -> dict[str, int]:
160
+ counts: dict[str, int] = {}
161
+ for item in layer_stats:
162
+ method = str(item["method"])
163
+ counts[method] = counts.get(method, 0) + 1
164
+ return dict(sorted(counts.items()))
165
+
166
+
167
+ def parse_args() -> argparse.Namespace:
168
+ parser = argparse.ArgumentParser()
169
+ parser.add_argument("--scan-json", required=True)
170
+ parser.add_argument("--model-dir", default="models/gemma-4-E2B")
171
+ parser.add_argument("--output", default="quantized/gemma_mixed_budget.pt")
172
+ parser.add_argument("--group-size", type=int, default=None)
173
+ parser.add_argument("--max-layers", type=int, default=None)
174
+ parser.add_argument("--skip-reconstruction-metrics", action="store_true")
175
+ return parser.parse_args()
176
+
177
+
178
+ def main() -> None:
179
+ args = parse_args()
180
+ scan_path = Path(args.scan_json)
181
+ scan = json.loads(scan_path.read_text(encoding="utf-8"))
182
+ scan["source_scan"] = str(scan_path)
183
+ group_size = args.group_size if args.group_size is not None else int(scan.get("group_size", 128))
184
+ max_layers = args.max_layers if args.max_layers is not None else scan.get("max_layers")
185
+
186
+ checkpoint = build_checkpoint(
187
+ scan=scan,
188
+ model_dir=Path(args.model_dir),
189
+ group_size=group_size,
190
+ max_layers=max_layers,
191
+ skip_reconstruction_metrics=args.skip_reconstruction_metrics,
192
+ )
193
+
194
+ output = Path(args.output)
195
+ output.parent.mkdir(parents=True, exist_ok=True)
196
+ torch.save(checkpoint, output)
197
+
198
+ stats = checkpoint["stats"]
199
+ print()
200
+ print("=" * 72)
201
+ print("MIXED BUDGET RESULTS")
202
+ print("=" * 72)
203
+ print(f"Layers: {stats['layers']}")
204
+ print(f"Average BPW: {stats['avg_bpw']:.4f}")
205
+ print(f"Compression vs BF16: {stats['compression_vs_bf16']:.2f}x")
206
+ if stats["weighted_mse"] is not None:
207
+ print(f"Weighted MSE: {stats['weighted_mse']:.8f}")
208
+ print(f"Weighted RMSE: {stats['weighted_rmse']:.6f}")
209
+ print("Method counts:", json.dumps(stats["method_counts"], sort_keys=True))
210
+ print("Saved:", output)
211
+
212
+
213
+ if __name__ == "__main__":
214
+ main()
src/Sub1BitLLM.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from dataclasses import dataclass
4
+ from typing import Dict, Optional, List, Iterator, Tuple, Any
5
+ from pathlib import Path
6
+ import os
7
+ import numpy as np
8
+
9
+ from .gguf_writer import GGUFWriter, GGML_TYPES
10
+ from .quantization import dequantize_factor
11
+
12
+
13
+ @dataclass
14
+ class Sub1BitConfig:
15
+ codebook_dim: int = 128
16
+ energy_threshold: float = 0.95
17
+ rank: int = 16
18
+ U_bits: float = 0.5
19
+ S_bits: float = 2.0
20
+ Vt_bits: float = 0.5
21
+ model_name: str = "llama-2-7b-sub1bit"
22
+ architecture: str = "llama"
23
+
24
+
25
+ class LowRankFactor(torch.nn.Module):
26
+ def __init__(self, U: torch.Tensor, S: torch.Tensor, Vt: torch.Tensor):
27
+ super().__init__()
28
+ self.register_buffer('U', U.half())
29
+ self.register_buffer('S', S.half())
30
+ self.register_buffer('Vt', Vt.half())
31
+ self.rank = U.shape[1]
32
+
33
+ def forward(self) -> torch.Tensor:
34
+ return torch.matmul(self.U * self.S.unsqueeze(0), self.Vt)
35
+
36
+ def forward_lowrank(self, x: torch.Tensor) -> torch.Tensor:
37
+ return torch.matmul(torch.matmul(x, self.Vt.T) * self.S.unsqueeze(0), self.U.T)
38
+
39
+
40
+ class TernaryQuantizedFactor(torch.nn.Module):
41
+ def __init__(self, data: torch.Tensor, scale: torch.Tensor, rank: int):
42
+ super().__init__()
43
+ self.register_buffer('data', data)
44
+ self.register_buffer('scale', scale)
45
+ self.rank = rank
46
+
47
+ def forward(self) -> torch.Tensor:
48
+ return self.data.float() * self.scale
49
+
50
+
51
+ class Sub1BitLLM(torch.nn.Module):
52
+ def __init__(
53
+ self,
54
+ model_path: str,
55
+ config: Optional[Sub1BitConfig] = None,
56
+ device: str = "cuda" if torch.cuda.is_available() else "cpu"
57
+ ):
58
+ super().__init__()
59
+ self.model_path = model_path
60
+ self.config = config or Sub1BitConfig()
61
+ self.device = device
62
+ self.layers: Dict[int, LowRankFactor] = {}
63
+ self.metadata: Dict = {}
64
+
65
+ @classmethod
66
+ def from_fp16(
67
+ cls,
68
+ model_path: str,
69
+ config: Optional[Sub1BitConfig] = None,
70
+ checkpoint_dir: Optional[str] = None,
71
+ device: str = "cuda" if torch.cuda.is_available() else "cpu"
72
+ ) -> "Sub1BitLLM":
73
+ instance = cls(model_path, config, device)
74
+ if checkpoint_dir is None:
75
+ checkpoint_dir = Path(model_path).parent / "checkpoints"
76
+ else:
77
+ checkpoint_dir = Path(checkpoint_dir)
78
+ if not checkpoint_dir.exists():
79
+ raise FileNotFoundError(f"Checkpoint directory not found: {checkpoint_dir}")
80
+ for ckpt_file in sorted(checkpoint_dir.glob("layer_*.pt")):
81
+ layer_idx = int(ckpt_file.stem.split("_")[1])
82
+ factor = torch.load(ckpt_file, weights_only=False, map_location=device)
83
+ U = torch.from_numpy(factor['U']).to(device)
84
+ S = torch.from_numpy(factor['S']).to(device)
85
+ Vt = torch.from_numpy(factor['Vt']).to(device)
86
+ instance.layers[layer_idx] = LowRankFactor(U, S, Vt)
87
+ instance.metadata = {
88
+ 'num_layers': len(instance.layers),
89
+ 'rank': instance.config.rank,
90
+ 'energy_threshold': instance.config.energy_threshold
91
+ }
92
+ return instance
93
+
94
+ def load_checkpoint(self, checkpoint_path: str) -> "Sub1BitLLM":
95
+ checkpoint = torch.load(checkpoint_path, map_location=self.device, weights_only=False)
96
+ if 'layers' in checkpoint:
97
+ for layer_idx, factor_data in checkpoint['layers'].items():
98
+ self.layers[int(layer_idx)] = LowRankFactor(
99
+ torch.from_numpy(factor_data['U']).to(self.device),
100
+ torch.from_numpy(factor_data['S']).to(self.device),
101
+ torch.from_numpy(factor_data['Vt']).to(self.device),
102
+ )
103
+ return self
104
+
105
+ def state_dict(self) -> Dict[str, torch.Tensor]:
106
+ state = {}
107
+ for layer_idx, layer in self.layers.items():
108
+ state[f'layers.{layer_idx}.U'] = layer.U
109
+ state[f'layers.{layer_idx}.S'] = layer.S
110
+ state[f'layers.{layer_idx}.Vt'] = layer.Vt
111
+ return state
112
+
113
+ def forward(self, x: torch.Tensor, layer_indices: Optional[List[int]] = None) -> Dict[int, torch.Tensor]:
114
+ outputs = {}
115
+ indices = layer_indices if layer_indices is not None else list(self.layers.keys())
116
+ for idx in indices:
117
+ if idx in self.layers:
118
+ outputs[idx] = self.layers[idx].forward_lowrank(x)
119
+ return outputs
120
+
121
+ def get_weight(self, layer_idx: int) -> torch.Tensor:
122
+ if layer_idx not in self.layers:
123
+ raise KeyError(f"Layer {layer_idx} not found")
124
+ return self.layers[layer_idx]()
125
+
126
+ def iter_layers(self) -> Iterator[Tuple[int, LowRankFactor]]:
127
+ for idx in sorted(self.layers.keys()):
128
+ yield idx, self.layers[idx]
129
+
130
+ def compression_stats(self) -> Dict[str, float]:
131
+ total_original = 0
132
+ total_factor = 0
133
+ for _, layer in self.iter_layers():
134
+ orig_size = layer.U.shape[0] * layer.Vt.shape[1]
135
+ factor_size = layer.U.numel() + layer.S.numel() + layer.Vt.numel()
136
+ total_original += orig_size
137
+ total_factor += factor_size
138
+ return {
139
+ 'compression_ratio': total_original / total_factor if total_factor > 0 else 0,
140
+ 'avg_rank': sum(l.rank for _, l in self.iter_layers()) / max(len(self.layers), 1)
141
+ }
142
+
143
+ def to_gguf(self, output_path: str, metadata: Optional[Dict] = None):
144
+ writer = GGUFWriter(output_path)
145
+ writer.add_key_value("general.architecture", self.config.architecture)
146
+ writer.add_key_value("general.name", self.config.model_name)
147
+ writer.add_key_value("quantization.type", "sub1bit_lowrank")
148
+ writer.add_key_value("quantization.U_bits", self.config.U_bits)
149
+ writer.add_key_value("quantization.S_bits", self.config.S_bits)
150
+ writer.add_key_value("quantization.Vt_bits", self.config.Vt_bits)
151
+ if metadata:
152
+ for key, value in metadata.items():
153
+ writer.add_key_value(key, value)
154
+ for layer_idx, layer in self.iter_layers():
155
+ writer.add_tensor(
156
+ f"model.layers.{layer_idx}.U",
157
+ layer.U.cpu().numpy().astype(np.float16),
158
+ GGML_TYPES['float16']
159
+ )
160
+ writer.add_tensor(
161
+ f"model.layers.{layer_idx}.S",
162
+ layer.S.cpu().numpy().astype(np.float16),
163
+ GGML_TYPES['float16']
164
+ )
165
+ writer.add_tensor(
166
+ f"model.layers.{layer_idx}.Vt",
167
+ layer.Vt.cpu().numpy().astype(np.float16),
168
+ GGML_TYPES['float16']
169
+ )
170
+ writer.add_tensor(
171
+ f"model.layers.{layer_idx}.rank",
172
+ np.array([layer.rank], dtype=np.int32),
173
+ GGML_TYPES['int32']
174
+ )
175
+ writer.write()
176
+ return os.path.getsize(output_path)
177
+
178
+
179
+ def from_fp16(
180
+ model_path: str,
181
+ config: Optional[Sub1BitConfig] = None,
182
+ checkpoint_dir: Optional[str] = None,
183
+ device: str = "cuda" if torch.cuda.is_available() else "cpu"
184
+ ) -> Sub1BitLLM:
185
+ return Sub1BitLLM.from_fp16(model_path, config, checkpoint_dir, device)
186
+
187
+
188
+ if __name__ == "__main__":
189
+ import argparse
190
+ parser = argparse.ArgumentParser(description="Sub1BitLLM API Demo")
191
+ parser.add_argument("--model", type=str, required=True, help="Path to model weights")
192
+ parser.add_argument("--checkpoint-dir", type=str, default=None, help="Path to checkpoint directory")
193
+ parser.add_argument("--device", type=str, default="cuda", help="Device")
194
+ args = parser.parse_args()
195
+ config = Sub1BitConfig(
196
+ codebook_dim=128,
197
+ energy_threshold=0.95,
198
+ model_name="llama-2-7b-sub1bit"
199
+ )
200
+ model = from_fp16(args.model, config=config, checkpoint_dir=args.checkpoint_dir, device=args.device)
201
+ print(f"Loaded Sub1BitLLM with {len(model.layers)} layers")
202
+ print(f"Compression stats: {model.compression_stats()}")
src/__init__.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from typing import Dict, Optional, List
3
+ from .Sub1BitLLM import Sub1BitLLM, Sub1BitConfig, from_fp16
4
+ from .lowrank_factorization import low_rank_factorize, factorize_model_weights, compute_optimal_rank
5
+ from .quantization import (
6
+ ternary_quantize, ternary_pack, ternary_unpack,
7
+ sigma_quantize, sigma_dequantize,
8
+ quantize_factor, pack_factor, unpack_factor, dequantize_factor,
9
+ )
10
+ from .groupwise_int4 import (
11
+ dequantize_groupwise_int4,
12
+ estimate_groupwise_int4_bpw,
13
+ pack_signed_int4,
14
+ quantize_groupwise_int4,
15
+ unpack_signed_int4,
16
+ )
17
+ from .mixed_budget import allocate_mixed_budget, summarize_allocation
18
+ from .error_budget_residual import (
19
+ dequantize_binary_residual,
20
+ dequantize_error_budget_residual,
21
+ estimate_binary_residual_bpw,
22
+ estimate_error_budget_residual_bpw,
23
+ quantize_binary_residual,
24
+ quantize_error_budget_residual,
25
+ )
26
+ from .gguf_writer import GGUFWriter, GGML_TYPES, GGUF_TYPES
27
+ from .pack_gguf import pack_sub1bit_model, QuantizedLayer
28
+
29
+ __all__ = [
30
+ "Sub1BitLLM",
31
+ "Sub1BitConfig",
32
+ "from_fp16",
33
+ "low_rank_factorize",
34
+ "factorize_model_weights",
35
+ "compute_optimal_rank",
36
+ "ternary_quantize",
37
+ "ternary_pack",
38
+ "ternary_unpack",
39
+ "sigma_quantize",
40
+ "sigma_dequantize",
41
+ "quantize_factor",
42
+ "pack_factor",
43
+ "unpack_factor",
44
+ "dequantize_factor",
45
+ "dequantize_groupwise_int4",
46
+ "estimate_groupwise_int4_bpw",
47
+ "pack_signed_int4",
48
+ "quantize_groupwise_int4",
49
+ "unpack_signed_int4",
50
+ "allocate_mixed_budget",
51
+ "summarize_allocation",
52
+ "dequantize_binary_residual",
53
+ "dequantize_error_budget_residual",
54
+ "estimate_binary_residual_bpw",
55
+ "estimate_error_budget_residual_bpw",
56
+ "quantize_binary_residual",
57
+ "quantize_error_budget_residual",
58
+ "GGUFWriter",
59
+ "GGML_TYPES",
60
+ "GGUF_TYPES",
61
+ "pack_sub1bit_model",
62
+ "QuantizedLayer",
63
+ ]
src/error_budget_residual.py ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from math import ceil, log2
4
+ from typing import Sequence
5
+
6
+ import torch
7
+
8
+ from .groupwise_int4 import pack_signed_int4, unpack_signed_int4
9
+
10
+
11
+ def pack_signed_int2(values: torch.Tensor) -> torch.Tensor:
12
+ """Pack signed INT2 values in {-1, 0, 1}, four values per byte."""
13
+ flat = values.to(torch.int8).flatten()
14
+ if flat.numel() and (flat.min().item() < -1 or flat.max().item() > 1):
15
+ raise ValueError("signed int2 values must be in {-1, 0, 1}")
16
+ encoded = (flat + 1).to(torch.uint8)
17
+ pad = (-encoded.numel()) % 4
18
+ if pad:
19
+ encoded = torch.cat([encoded, torch.ones(pad, dtype=torch.uint8, device=encoded.device)])
20
+ encoded = encoded.reshape(-1, 4)
21
+ return (
22
+ encoded[:, 0]
23
+ | torch.bitwise_left_shift(encoded[:, 1], 2)
24
+ | torch.bitwise_left_shift(encoded[:, 2], 4)
25
+ | torch.bitwise_left_shift(encoded[:, 3], 6)
26
+ ).contiguous()
27
+
28
+
29
+ def unpack_signed_int2(packed: torch.Tensor, count: int) -> torch.Tensor:
30
+ """Unpack bytes produced by pack_signed_int2."""
31
+ if count < 0:
32
+ raise ValueError("count must be non-negative")
33
+ packed = packed.to(torch.uint8).flatten()
34
+ values = torch.stack(
35
+ (
36
+ torch.bitwise_and(packed, 0x03),
37
+ torch.bitwise_and(torch.bitwise_right_shift(packed, 2), 0x03),
38
+ torch.bitwise_and(torch.bitwise_right_shift(packed, 4), 0x03),
39
+ torch.bitwise_and(torch.bitwise_right_shift(packed, 6), 0x03),
40
+ ),
41
+ dim=1,
42
+ ).flatten()[:count]
43
+ return values.to(torch.int16).sub(1).to(torch.int8)
44
+
45
+
46
+ def pack_binary_sign(values: torch.Tensor) -> torch.Tensor:
47
+ """Pack residual signs {-1, +1}, eight signs per byte."""
48
+ flat = values.to(torch.int8).flatten()
49
+ if flat.numel() and not torch.all((flat == -1) | (flat == 1) | (flat == 0)):
50
+ raise ValueError("binary residual signs must be -1, 0, or 1")
51
+ encoded = torch.where(flat > 0, torch.ones_like(flat, dtype=torch.uint8), torch.zeros_like(flat, dtype=torch.uint8))
52
+ pad = (-encoded.numel()) % 8
53
+ if pad:
54
+ encoded = torch.cat([encoded, torch.zeros(pad, dtype=torch.uint8, device=encoded.device)])
55
+ encoded = encoded.reshape(-1, 8)
56
+ shifts = torch.arange(8, dtype=torch.uint8, device=encoded.device)
57
+ return torch.sum(torch.bitwise_left_shift(encoded, shifts), dim=1).to(torch.uint8).contiguous()
58
+
59
+
60
+ def unpack_binary_sign(packed: torch.Tensor, count: int) -> torch.Tensor:
61
+ """Unpack bytes produced by pack_binary_sign into {-1, +1} signs."""
62
+ if count < 0:
63
+ raise ValueError("count must be non-negative")
64
+ packed = packed.to(torch.uint8).flatten()
65
+ shifts = torch.arange(8, dtype=torch.uint8, device=packed.device)
66
+ bits = torch.bitwise_and(torch.bitwise_right_shift(packed.unsqueeze(1), shifts), 1).flatten()[:count]
67
+ return torch.where(bits > 0, torch.ones_like(bits, dtype=torch.int8), -torch.ones_like(bits, dtype=torch.int8))
68
+
69
+
70
+ def _validate_2d_shape(shape: Sequence[int]) -> tuple[int, int]:
71
+ if len(shape) != 2:
72
+ raise ValueError("shape must be a 2D weight matrix shape")
73
+ rows, cols = int(shape[0]), int(shape[1])
74
+ if rows <= 0 or cols <= 0:
75
+ raise ValueError("shape dimensions must be positive")
76
+ return rows, cols
77
+
78
+
79
+ def estimate_binary_residual_bpw(
80
+ shape: Sequence[int],
81
+ group_size: int = 128,
82
+ scale_bits: int = 16,
83
+ ) -> float:
84
+ """Estimate BPW for INT2 base + 1-bit residual signs + two group scales."""
85
+ if group_size <= 0:
86
+ raise ValueError("group_size must be positive")
87
+ rows, cols = _validate_2d_shape(shape)
88
+ groups_per_row = ceil(cols / group_size)
89
+ weight_bits = rows * cols * 3
90
+ scale_bits_total = rows * groups_per_row * scale_bits * 2
91
+ return (weight_bits + scale_bits_total) / (rows * cols)
92
+
93
+
94
+ def estimate_error_budget_residual_bpw(
95
+ shape: Sequence[int],
96
+ group_size: int = 128,
97
+ outliers_per_group: int = 8,
98
+ scale_bits: int = 16,
99
+ correction_bits: int = 4,
100
+ ) -> float:
101
+ """Estimate BPW for INT2 base plus sparse residual corrections.
102
+
103
+ Each group stores a base scale, a residual correction scale, and up to
104
+ outliers_per_group index+signed-correction pairs.
105
+ """
106
+ if group_size <= 0:
107
+ raise ValueError("group_size must be positive")
108
+ if outliers_per_group < 0:
109
+ raise ValueError("outliers_per_group must be non-negative")
110
+ rows, cols = _validate_2d_shape(shape)
111
+ groups_per_row = ceil(cols / group_size)
112
+ groups = rows * groups_per_row
113
+ index_bits = max(1, ceil(log2(group_size)))
114
+ base_and_binary_bits = rows * cols * 3
115
+ base_and_binary_scale_bits = groups * scale_bits * 2
116
+ outlier_scale_bits = groups * scale_bits if outliers_per_group else 0
117
+ outlier_bits = groups * outliers_per_group * (index_bits + correction_bits)
118
+ return (base_and_binary_bits + base_and_binary_scale_bits + outlier_scale_bits + outlier_bits) / (rows * cols)
119
+
120
+
121
+ def _reshape_blocks(weight: torch.Tensor, group_size: int) -> tuple[torch.Tensor, int]:
122
+ if weight.ndim != 2:
123
+ raise ValueError("weight must be a 2D tensor")
124
+ if group_size <= 0:
125
+ raise ValueError("group_size must be positive")
126
+
127
+ source = weight.detach().to(torch.float32)
128
+ rows, cols = source.shape
129
+ groups_per_row = ceil(cols / group_size)
130
+ padded_cols = groups_per_row * group_size
131
+ if padded_cols != cols:
132
+ padded = torch.zeros((rows, padded_cols), dtype=source.dtype, device=source.device)
133
+ padded[:, :cols] = source
134
+ source = padded
135
+ return source.reshape(rows, groups_per_row, group_size), padded_cols
136
+
137
+
138
+ def quantize_binary_residual(
139
+ weight: torch.Tensor,
140
+ group_size: int = 128,
141
+ scale_dtype: torch.dtype = torch.float16,
142
+ ) -> dict:
143
+ """Quantize with an INT2 base and per-group 1-bit residual correction.
144
+
145
+ The residual code stores only the sign of the base reconstruction error plus
146
+ one learned magnitude per group. This is a compact candidate below 4 BPW.
147
+ """
148
+ blocks, padded_cols = _reshape_blocks(weight, group_size)
149
+ rows, cols = weight.shape
150
+
151
+ base_max = blocks.abs().amax(dim=2)
152
+ safe_max = torch.where(base_max > 0, base_max, torch.ones_like(base_max))
153
+ multipliers = torch.tensor([0.375, 0.5, 0.625, 0.75, 0.875, 1.0], dtype=torch.float32, device=blocks.device)
154
+
155
+ best_sse = torch.full_like(base_max, float("inf"))
156
+ best_q = torch.zeros_like(blocks, dtype=torch.int8)
157
+ best_base_scales = torch.ones_like(base_max)
158
+ best_residual_sign = torch.zeros_like(blocks, dtype=torch.int8)
159
+ best_residual_scales = torch.zeros_like(base_max)
160
+
161
+ for multiplier in multipliers:
162
+ candidate_scales = (safe_max * multiplier).to(scale_dtype).to(torch.float32)
163
+ q = torch.round(blocks / candidate_scales.unsqueeze(-1)).clamp(-1, 1)
164
+ base_rec = q * candidate_scales.unsqueeze(-1)
165
+ residual = blocks - base_rec
166
+ residual_scales = residual.abs().mean(dim=2).to(scale_dtype).to(torch.float32)
167
+ residual_sign = torch.where(residual >= 0, 1.0, -1.0)
168
+ residual_rec = residual_sign * residual_scales.unsqueeze(-1)
169
+ sse = (residual - residual_rec).pow(2).sum(dim=2)
170
+
171
+ better = sse < best_sse
172
+ best_sse = torch.where(better, sse, best_sse)
173
+ best_base_scales = torch.where(better, candidate_scales, best_base_scales)
174
+ best_residual_scales = torch.where(better, residual_scales, best_residual_scales)
175
+ best_q = torch.where(better.unsqueeze(-1), q.to(torch.int8), best_q)
176
+ best_residual_sign = torch.where(better.unsqueeze(-1), residual_sign.to(torch.int8), best_residual_sign)
177
+
178
+ zero_groups = base_max <= 0
179
+ best_residual_sign = torch.where(
180
+ zero_groups.unsqueeze(-1),
181
+ torch.zeros_like(best_residual_sign),
182
+ best_residual_sign,
183
+ )
184
+ best_residual_scales = torch.where(zero_groups, torch.zeros_like(best_residual_scales), best_residual_scales)
185
+
186
+ return {
187
+ "format": "int2_binary_residual",
188
+ "base_bits": 2,
189
+ "residual_bits": 1,
190
+ "group_size": int(group_size),
191
+ "orig_shape": [int(rows), int(cols)],
192
+ "padded_shape": [int(rows), int(padded_cols)],
193
+ "base_packed": pack_signed_int2(best_q.cpu()),
194
+ "base_count": int(best_q.numel()),
195
+ "base_scales": best_base_scales.to(scale_dtype).cpu(),
196
+ "residual_packed": pack_binary_sign(best_residual_sign.cpu()),
197
+ "residual_count": int(best_residual_sign.numel()),
198
+ "residual_scales": best_residual_scales.to(scale_dtype).cpu(),
199
+ "bpw": estimate_binary_residual_bpw((rows, cols), group_size=group_size, scale_bits=16),
200
+ }
201
+
202
+
203
+ def _valid_block_mask(rows: int, cols: int, padded_cols: int, group_size: int, device: torch.device) -> torch.Tensor:
204
+ valid = torch.zeros((rows, padded_cols), dtype=torch.bool, device=device)
205
+ valid[:, :cols] = True
206
+ return valid.reshape(rows, padded_cols // group_size, group_size)
207
+
208
+
209
+ def quantize_error_budget_residual(
210
+ weight: torch.Tensor,
211
+ group_size: int = 128,
212
+ outliers_per_group: int = 8,
213
+ scale_dtype: torch.dtype = torch.float16,
214
+ ) -> dict:
215
+ """Quantize with an INT2 base plus sparse top-error residual corrections."""
216
+ if outliers_per_group < 0:
217
+ raise ValueError("outliers_per_group must be non-negative")
218
+ if group_size > 256:
219
+ raise ValueError("group_size must be <= 256 so residual indices fit in uint8")
220
+
221
+ blocks, padded_cols = _reshape_blocks(weight, group_size)
222
+ rows, cols = weight.shape
223
+ groups_per_row = padded_cols // group_size
224
+ valid_blocks = _valid_block_mask(rows, cols, padded_cols, group_size, blocks.device)
225
+ k = min(int(outliers_per_group), group_size)
226
+
227
+ base_max = blocks.abs().amax(dim=2)
228
+ safe_max = torch.where(base_max > 0, base_max, torch.ones_like(base_max))
229
+ multipliers = torch.tensor([0.375, 0.5, 0.625, 0.75, 0.875, 1.0], dtype=torch.float32, device=blocks.device)
230
+
231
+ best_sse = torch.full_like(base_max, float("inf"))
232
+ best_q = torch.zeros_like(blocks, dtype=torch.int8)
233
+ best_base_scales = torch.ones_like(base_max)
234
+ best_binary_sign = torch.zeros_like(blocks, dtype=torch.int8)
235
+ best_binary_scales = torch.zeros_like(base_max)
236
+ best_indices = torch.zeros((rows, groups_per_row, k), dtype=torch.long, device=blocks.device)
237
+ best_counts = torch.zeros((rows, groups_per_row), dtype=torch.long, device=blocks.device)
238
+ best_outlier_q = torch.zeros((rows, groups_per_row, k), dtype=torch.int8, device=blocks.device)
239
+ best_outlier_scales = torch.zeros_like(base_max)
240
+
241
+ for multiplier in multipliers:
242
+ candidate_scales = (safe_max * multiplier).to(scale_dtype).to(torch.float32)
243
+ q = torch.round(blocks / candidate_scales.unsqueeze(-1)).clamp(-1, 1)
244
+ base_rec = q * candidate_scales.unsqueeze(-1)
245
+ residual = blocks - base_rec
246
+ binary_scales = residual.abs().mean(dim=2).to(scale_dtype).to(torch.float32)
247
+ binary_sign = torch.where(residual >= 0, 1.0, -1.0)
248
+ binary_rec = binary_sign * binary_scales.unsqueeze(-1)
249
+ remaining = residual - binary_rec
250
+
251
+ outlier_rec = torch.zeros_like(blocks)
252
+ top_idx = torch.zeros((rows, groups_per_row, k), dtype=torch.long, device=blocks.device)
253
+ selected_counts = torch.zeros((rows, groups_per_row), dtype=torch.long, device=blocks.device)
254
+ outlier_q = torch.zeros((rows, groups_per_row, k), dtype=torch.int8, device=blocks.device)
255
+ outlier_scales = torch.zeros_like(base_max)
256
+
257
+ if k:
258
+ masked_abs = torch.where(valid_blocks, remaining.abs(), torch.full_like(remaining, -1.0))
259
+ top_abs, top_idx = torch.topk(masked_abs, k=k, dim=2)
260
+ selected = top_abs >= 0
261
+ selected_counts = selected.sum(dim=2)
262
+ gathered = torch.gather(remaining, 2, top_idx)
263
+ max_abs = torch.where(top_abs[:, :, 0] > 0, top_abs[:, :, 0], torch.zeros_like(top_abs[:, :, 0]))
264
+ outlier_scales = (max_abs / 7.0).to(scale_dtype).to(torch.float32)
265
+ denom = torch.where(outlier_scales > 0, outlier_scales, torch.ones_like(outlier_scales))
266
+ outlier_q = torch.round(gathered / denom.unsqueeze(-1)).clamp(-7, 7).to(torch.int8)
267
+ outlier_q = torch.where(selected, outlier_q, torch.zeros_like(outlier_q))
268
+ correction = outlier_q.to(torch.float32) * outlier_scales.unsqueeze(-1)
269
+ correction = torch.where(selected, correction, torch.zeros_like(correction))
270
+ outlier_rec.scatter_add_(2, top_idx, correction)
271
+
272
+ error = torch.where(valid_blocks, base_rec + binary_rec + outlier_rec - blocks, torch.zeros_like(blocks))
273
+ sse = error.pow(2).sum(dim=2)
274
+ better = sse < best_sse
275
+ best_sse = torch.where(better, sse, best_sse)
276
+ best_base_scales = torch.where(better, candidate_scales, best_base_scales)
277
+ best_q = torch.where(better.unsqueeze(-1), q.to(torch.int8), best_q)
278
+ best_binary_sign = torch.where(better.unsqueeze(-1), binary_sign.to(torch.int8), best_binary_sign)
279
+ best_binary_scales = torch.where(better, binary_scales, best_binary_scales)
280
+ best_indices = torch.where(better.unsqueeze(-1), top_idx, best_indices)
281
+ best_counts = torch.where(better, selected_counts, best_counts)
282
+ best_outlier_q = torch.where(better.unsqueeze(-1), outlier_q, best_outlier_q)
283
+ best_outlier_scales = torch.where(better, outlier_scales, best_outlier_scales)
284
+
285
+ return {
286
+ "format": "int2_error_budget_residual",
287
+ "base_bits": 2,
288
+ "correction_bits": 4,
289
+ "group_size": int(group_size),
290
+ "outliers_per_group": int(outliers_per_group),
291
+ "orig_shape": [int(rows), int(cols)],
292
+ "padded_shape": [int(rows), int(padded_cols)],
293
+ "base_packed": pack_signed_int2(best_q.cpu()),
294
+ "base_count": int(best_q.numel()),
295
+ "base_scales": best_base_scales.to(scale_dtype).cpu(),
296
+ "binary_residual_packed": pack_binary_sign(best_binary_sign.cpu()),
297
+ "binary_residual_count": int(best_binary_sign.numel()),
298
+ "binary_residual_scales": best_binary_scales.to(scale_dtype).cpu(),
299
+ "outlier_indices": best_indices.to(torch.uint8).cpu(),
300
+ "outlier_counts": best_counts.to(torch.uint8).cpu(),
301
+ "outlier_packed": pack_signed_int4(best_outlier_q.cpu()),
302
+ "outlier_count": int(best_outlier_q.numel()),
303
+ "outlier_scales": best_outlier_scales.to(scale_dtype).cpu(),
304
+ "bpw": estimate_error_budget_residual_bpw(
305
+ (rows, cols),
306
+ group_size=group_size,
307
+ outliers_per_group=outliers_per_group,
308
+ scale_bits=16,
309
+ correction_bits=4,
310
+ ),
311
+ }
312
+
313
+
314
+ def dequantize_binary_residual(
315
+ entry: dict,
316
+ device: str | torch.device = "cpu",
317
+ include_residual: bool = True,
318
+ ) -> torch.Tensor:
319
+ """Dequantize an INT2 base plus optional 1-bit residual correction."""
320
+ rows, cols = [int(dim) for dim in entry["orig_shape"]]
321
+ padded_rows, padded_cols = [int(dim) for dim in entry.get("padded_shape", entry["orig_shape"])]
322
+ group_size = int(entry["group_size"])
323
+ if padded_rows != rows:
324
+ raise ValueError("padded row count must match original row count")
325
+ if padded_cols % group_size != 0:
326
+ raise ValueError("padded columns must be divisible by group_size")
327
+
328
+ groups_per_row = padded_cols // group_size
329
+ if "base_packed" in entry:
330
+ base_q = unpack_signed_int2(entry["base_packed"].to(device), int(entry["base_count"]))
331
+ else:
332
+ base_q = entry["base_q"].to(device=device, dtype=torch.int8).flatten()
333
+ base_q = base_q.to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, group_size)
334
+ base_scales = entry["base_scales"].to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, 1)
335
+ restored = base_q * base_scales
336
+ if include_residual:
337
+ if "residual_packed" in entry:
338
+ residual_sign = unpack_binary_sign(entry["residual_packed"].to(device), int(entry["residual_count"]))
339
+ else:
340
+ residual_sign = entry["residual_sign"].to(device=device, dtype=torch.int8).flatten()
341
+ residual_sign = residual_sign.to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, group_size)
342
+ residual_scales = entry["residual_scales"].to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, 1)
343
+ restored = restored + residual_sign * residual_scales
344
+
345
+ return restored.reshape(rows, padded_cols)[:, :cols].contiguous()
346
+
347
+
348
+ def dequantize_error_budget_residual(
349
+ entry: dict,
350
+ device: str | torch.device = "cpu",
351
+ include_residual: bool = True,
352
+ ) -> torch.Tensor:
353
+ """Dequantize an INT2 base plus sparse residual correction side channel."""
354
+ rows, cols = [int(dim) for dim in entry["orig_shape"]]
355
+ padded_rows, padded_cols = [int(dim) for dim in entry.get("padded_shape", entry["orig_shape"])]
356
+ group_size = int(entry["group_size"])
357
+ if padded_rows != rows:
358
+ raise ValueError("padded row count must match original row count")
359
+ if padded_cols % group_size != 0:
360
+ raise ValueError("padded columns must be divisible by group_size")
361
+
362
+ groups_per_row = padded_cols // group_size
363
+ base_q = unpack_signed_int2(entry["base_packed"].to(device), int(entry["base_count"]))
364
+ base_q = base_q.to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, group_size)
365
+ base_scales = entry["base_scales"].to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, 1)
366
+ restored = base_q * base_scales
367
+
368
+ if include_residual:
369
+ binary_sign = unpack_binary_sign(
370
+ entry["binary_residual_packed"].to(device),
371
+ int(entry["binary_residual_count"]),
372
+ )
373
+ binary_sign = binary_sign.to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, group_size)
374
+ binary_scales = entry["binary_residual_scales"].to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, 1)
375
+ restored = restored + binary_sign * binary_scales
376
+
377
+ k = int(entry["outliers_per_group"])
378
+ if k <= 0:
379
+ return restored.reshape(rows, padded_cols)[:, :cols].contiguous()
380
+
381
+ outlier_q = unpack_signed_int4(entry["outlier_packed"].to(device), int(entry["outlier_count"]))
382
+ outlier_q = outlier_q.to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, k)
383
+ indices = entry["outlier_indices"].to(device=device, dtype=torch.long).reshape(rows, groups_per_row, k)
384
+ counts = entry["outlier_counts"].to(device=device, dtype=torch.long).reshape(rows, groups_per_row)
385
+ outlier_scales = entry["outlier_scales"].to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, 1)
386
+
387
+ valid = torch.arange(k, device=device).reshape(1, 1, k) < counts.unsqueeze(-1)
388
+ correction = torch.where(valid, outlier_q * outlier_scales, torch.zeros_like(outlier_q))
389
+ restored_flat = restored.reshape(rows * groups_per_row, group_size)
390
+ group_ids = torch.arange(rows * groups_per_row, device=device).reshape(rows, groups_per_row, 1).expand_as(indices)
391
+ restored_flat.index_put_(
392
+ (group_ids.reshape(-1)[valid.reshape(-1)], indices.reshape(-1)[valid.reshape(-1)]),
393
+ correction.reshape(-1)[valid.reshape(-1)],
394
+ accumulate=True,
395
+ )
396
+
397
+ return restored.reshape(rows, padded_cols)[:, :cols].contiguous()
src/gguf_writer.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import struct
2
+ import os
3
+ import numpy as np
4
+ from typing import List, Tuple, Any
5
+
6
+
7
+ GGUF_MAGIC = b'GGUF'
8
+ GGUF_VERSION = 3
9
+
10
+
11
+ GGUF_TYPES = {
12
+ 'uint8': 0, 'int8': 1,
13
+ 'uint16': 2, 'int16': 3,
14
+ 'uint32': 4, 'int32': 5,
15
+ 'float32': 6, 'bool': 7,
16
+ 'string': 8, 'array': 9,
17
+ 'uint64': 10, 'int64': 11, 'float64': 12,
18
+ }
19
+
20
+
21
+ GGML_TYPES = {
22
+ 'float32': 0, 'float16': 1,
23
+ 'q8_0': 2, 'q4_0': 3, 'q4_1': 4,
24
+ 'int8': 5, 'int16': 6, 'int32': 7,
25
+ }
26
+
27
+
28
+ class GGUFWriter:
29
+ def __init__(self, filepath: str):
30
+ self.filepath = filepath
31
+ self.kv_pairs: List[Tuple[str, Any]] = []
32
+ self.tensors: List[Tuple[str, np.ndarray, int]] = []
33
+
34
+ def add_key_value(self, key: str, value: Any):
35
+ self.kv_pairs.append((key, value))
36
+
37
+ def add_tensor(self, name: str, data: np.ndarray, ggml_type: int = 0):
38
+ self.tensors.append((name, data, ggml_type))
39
+
40
+ def write(self):
41
+ os.makedirs(os.path.dirname(self.filepath) or ".", exist_ok=True)
42
+ with open(self.filepath, 'wb') as f:
43
+ f.write(GGUF_MAGIC)
44
+ f.write(struct.pack('<I', GGUF_VERSION))
45
+ f.write(struct.pack('<Q', len(self.tensors)))
46
+ f.write(struct.pack('<Q', len(self.kv_pairs)))
47
+ for key, value in self.kv_pairs:
48
+ self._write_string(f, key)
49
+ self._write_value(f, value)
50
+ sizes = [data.nbytes for _, data, _ in self.tensors]
51
+ data_offset = f.tell() + sum(
52
+ 8 + len(name.encode('utf-8')) + 4 + 8 * data.ndim + 4 + 8
53
+ for name, data, _ in self.tensors
54
+ )
55
+ for i, (name, data, ggml_type) in enumerate(self.tensors):
56
+ self._write_string(f, name)
57
+ f.write(struct.pack('<I', data.ndim))
58
+ for dim in data.shape:
59
+ f.write(struct.pack('<q', dim))
60
+ f.write(struct.pack('<I', ggml_type))
61
+ f.write(struct.pack('<Q', data_offset))
62
+ data_offset += sizes[i]
63
+ for _, data, _ in self.tensors:
64
+ f.write(data.tobytes())
65
+
66
+ def _write_string(self, f, s: str):
67
+ encoded = s.encode('utf-8')
68
+ f.write(struct.pack('<Q', len(encoded)))
69
+ f.write(encoded)
70
+
71
+ def _write_value(self, f, value: Any):
72
+ if isinstance(value, str):
73
+ f.write(struct.pack('<i', GGUF_TYPES['string']))
74
+ self._write_string(f, value)
75
+ elif isinstance(value, bool):
76
+ f.write(struct.pack('<i', GGUF_TYPES['bool']))
77
+ f.write(struct.pack('<b', 1 if value else 0))
78
+ elif isinstance(value, int):
79
+ f.write(struct.pack('<i', GGUF_TYPES['int32']))
80
+ f.write(struct.pack('<i', value))
81
+ elif isinstance(value, float):
82
+ f.write(struct.pack('<i', GGUF_TYPES['float32']))
83
+ f.write(struct.pack('<f', value))
84
+ elif isinstance(value, list):
85
+ f.write(struct.pack('<i', GGUF_TYPES['array']))
86
+ if value:
87
+ f.write(struct.pack('<i', GGUF_TYPES.get(type(value[0]).__name__, 8)))
88
+ f.write(struct.pack('<Q', len(value)))
89
+ for item in value:
90
+ if isinstance(item, str):
91
+ self._write_string(f, item)
92
+ else:
93
+ self._write_value(f, item)
94
+ else:
95
+ raise ValueError(f"Unsupported type: {type(value)}")
src/groupwise_int4.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from math import ceil
4
+ from typing import Sequence
5
+
6
+ import torch
7
+
8
+
9
+ INT4_QMAX = 7
10
+
11
+
12
+ def _validate_signed_int4(values: torch.Tensor) -> torch.Tensor:
13
+ flat = values.to(torch.int8).flatten()
14
+ if flat.numel() == 0:
15
+ return flat
16
+ if flat.min().item() < -8 or flat.max().item() > 7:
17
+ raise ValueError("signed int4 values must be in [-8, 7]")
18
+ return flat
19
+
20
+
21
+ def pack_signed_int4(values: torch.Tensor) -> torch.Tensor:
22
+ """Pack signed int4 values into uint8 bytes, two values per byte."""
23
+ flat = _validate_signed_int4(values)
24
+ encoded = (flat + 8).to(torch.uint8)
25
+ if encoded.numel() % 2:
26
+ encoded = torch.cat([encoded, torch.full((1,), 8, dtype=torch.uint8, device=encoded.device)])
27
+
28
+ low = encoded[0::2]
29
+ high = torch.bitwise_left_shift(encoded[1::2], 4)
30
+ return torch.bitwise_or(low, high).contiguous()
31
+
32
+
33
+ def unpack_signed_int4(packed: torch.Tensor, count: int) -> torch.Tensor:
34
+ """Unpack uint8 bytes produced by pack_signed_int4."""
35
+ if count < 0:
36
+ raise ValueError("count must be non-negative")
37
+ packed = packed.to(torch.uint8).flatten()
38
+ low = torch.bitwise_and(packed, 0x0F)
39
+ high = torch.bitwise_right_shift(packed, 4)
40
+ encoded = torch.stack((low, high), dim=1).flatten()[:count]
41
+ return encoded.to(torch.int16).sub(8).to(torch.int8)
42
+
43
+
44
+ def estimate_groupwise_int4_bpw(
45
+ shape: Sequence[int],
46
+ group_size: int = 128,
47
+ scale_bits: int = 16,
48
+ ) -> float:
49
+ """Estimate bits per weight for row-wise group INT4 plus per-group scales."""
50
+ if len(shape) != 2:
51
+ raise ValueError("shape must be a 2D weight matrix shape")
52
+ if group_size <= 0:
53
+ raise ValueError("group_size must be positive")
54
+
55
+ rows, cols = int(shape[0]), int(shape[1])
56
+ if rows <= 0 or cols <= 0:
57
+ raise ValueError("shape dimensions must be positive")
58
+
59
+ groups_per_row = ceil(cols / group_size)
60
+ weight_bits = rows * cols * 4
61
+ scale_overhead_bits = rows * groups_per_row * scale_bits
62
+ return (weight_bits + scale_overhead_bits) / (rows * cols)
63
+
64
+
65
+ def quantize_groupwise_int4(
66
+ weight: torch.Tensor,
67
+ group_size: int = 128,
68
+ scale_dtype: torch.dtype = torch.float16,
69
+ ) -> dict:
70
+ """Symmetric row-wise group INT4 quantization for 2D weight matrices.
71
+
72
+ This is an inference-oriented storage format: the packed nibbles and group
73
+ scales can be consumed by an INT4 kernel later, while still being easy to
74
+ dequantize for correctness/perplexity evaluation today.
75
+ """
76
+ if weight.ndim != 2:
77
+ raise ValueError("weight must be a 2D tensor")
78
+ if group_size <= 0:
79
+ raise ValueError("group_size must be positive")
80
+
81
+ source = weight.detach().to(torch.float32)
82
+ rows, cols = source.shape
83
+ groups_per_row = ceil(cols / group_size)
84
+ padded_cols = groups_per_row * group_size
85
+ if padded_cols != cols:
86
+ padded = torch.zeros((rows, padded_cols), dtype=source.dtype, device=source.device)
87
+ padded[:, :cols] = source
88
+ source = padded
89
+
90
+ blocks = source.reshape(rows, groups_per_row, group_size)
91
+ max_abs = blocks.abs().amax(dim=2)
92
+ scales = torch.where(
93
+ max_abs > 0,
94
+ max_abs / INT4_QMAX,
95
+ torch.ones_like(max_abs),
96
+ )
97
+ stored_scales = scales.to(scale_dtype).to(torch.float32)
98
+ q = torch.round(blocks / stored_scales.unsqueeze(-1)).clamp(-INT4_QMAX, INT4_QMAX).to(torch.int8)
99
+
100
+ return {
101
+ "format": "groupwise_int4",
102
+ "bits": 4,
103
+ "group_size": int(group_size),
104
+ "orig_shape": [int(rows), int(cols)],
105
+ "padded_shape": [int(rows), int(padded_cols)],
106
+ "scales": stored_scales.to(scale_dtype).cpu(),
107
+ "packed_int4": pack_signed_int4(q.cpu()),
108
+ "bpw": estimate_groupwise_int4_bpw((rows, cols), group_size=group_size, scale_bits=16),
109
+ }
110
+
111
+
112
+ def dequantize_groupwise_int4(entry: dict, device: str | torch.device = "cpu") -> torch.Tensor:
113
+ """Dequantize an entry produced by quantize_groupwise_int4."""
114
+ rows, cols = [int(dim) for dim in entry["orig_shape"]]
115
+ padded_rows, padded_cols = [int(dim) for dim in entry.get("padded_shape", entry["orig_shape"])]
116
+ group_size = int(entry["group_size"])
117
+ if padded_rows != rows:
118
+ raise ValueError("padded row count must match original row count")
119
+ if padded_cols % group_size != 0:
120
+ raise ValueError("padded columns must be divisible by group_size")
121
+
122
+ groups_per_row = padded_cols // group_size
123
+ count = rows * groups_per_row * group_size
124
+ q = unpack_signed_int4(entry["packed_int4"].to(device), count).to(torch.float32)
125
+ q = q.reshape(rows, groups_per_row, group_size)
126
+ scales = entry["scales"].to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, 1)
127
+ restored = (q * scales).reshape(rows, padded_cols)
128
+ return restored[:, :cols].contiguous()
src/lowrank_factorization.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ from typing import Dict, Tuple, List
4
+
5
+
6
+ def compute_optimal_rank(singular_values: torch.Tensor, energy_threshold: float = 0.95) -> int:
7
+ squared_sv = singular_values ** 2
8
+ cumulative = torch.cumsum(squared_sv, dim=0)
9
+ total = torch.sum(squared_sv)
10
+ normalized = cumulative / total
11
+
12
+ r = torch.searchsorted(normalized, energy_threshold).item() + 1
13
+ return min(r, len(singular_values))
14
+
15
+
16
+ def low_rank_factorize(weight: torch.Tensor, energy_threshold: float = 0.95) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, int]:
17
+ original_shape = weight.shape
18
+
19
+ if weight.dim() > 2:
20
+ weight = weight.reshape(-1, weight.shape[-1])
21
+
22
+ U, S, Vt = torch.linalg.svd(weight, full_matrices=False)
23
+
24
+ r = compute_optimal_rank(S, energy_threshold)
25
+
26
+ U_r = U[:, :r]
27
+ S_r = S[:r]
28
+ Vt_r = Vt[:r, :]
29
+
30
+ U_r = U_r.reshape(original_shape[0], r)
31
+ Vt_r = Vt_r.reshape(r, original_shape[-1])
32
+
33
+ return U_r, S_r, Vt_r, r
34
+
35
+
36
+ def factorize_model_weights(weights: Dict[int, torch.Tensor], energy_threshold: float = 0.95) -> Dict:
37
+ factors = {}
38
+
39
+ for layer_idx, W in weights.items():
40
+ U, S, Vt, r = low_rank_factorize(W, energy_threshold)
41
+ factors[layer_idx] = {
42
+ 'U': U,
43
+ 'S': S,
44
+ 'Vt': Vt,
45
+ 'rank': r,
46
+ 'original_shape': W.shape,
47
+ 'energy_captured': (S ** 2).sum().item() / ((W ** 2).sum().item() + 1e-8)
48
+ }
49
+
50
+ return factors
51
+
52
+
53
+ def reconstruct_weight(factors: Dict) -> torch.Tensor:
54
+ U = factors['U']
55
+ S = factors['S']
56
+ Vt = factors['Vt']
57
+
58
+ return torch.matmul(U * S.unsqueeze(0), Vt)
59
+
60
+
61
+ def compute_compression_ratio(original_size: int, factors: Dict) -> float:
62
+ U_size = sum(f['U'].numel() for f in factors.values())
63
+ S_size = sum(f['S'].numel() for f in factors.values())
64
+ Vt_size = sum(f['Vt'].numel() for f in factors.values())
65
+
66
+ low_rank_size = U_size + S_size + Vt_size
67
+ return original_size / low_rank_size
68
+
69
+
70
+ if __name__ == "__main__":
71
+ W = torch.randn(4096, 4096)
72
+
73
+ U, S, Vt, r = low_rank_factorize(W, energy_threshold=0.95)
74
+ print(f"Original shape: {W.shape}")
75
+ print(f"Rank: {r}")
76
+ print(f"U shape: {U.shape}, S shape: {S.shape}, Vt shape: {Vt.shape}")
77
+
78
+ reconstructed = torch.matmul(U * S.unsqueeze(0), Vt)
79
+ error = torch.norm(W - reconstructed) / torch.norm(W)
80
+ print(f"Reconstruction error: {error:.6f}")
81
+
82
+ weights = {0: torch.randn(4096, 4096), 1: torch.randn(4096, 4096)}
83
+ factors = factorize_model_weights(weights)
84
+ for layer_idx, f in factors.items():
85
+ print(f"Layer {layer_idx}: rank={f['rank']}, energy={f['energy_captured']:.4f}")
src/mixed_budget.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ from math import sqrt
5
+ from typing import Iterable
6
+
7
+
8
+ def _candidate_score(layer: dict, candidate: dict) -> dict:
9
+ params = int(layer["params"])
10
+ activation_weight = float(layer.get("activation_weight", 1.0))
11
+ scored = deepcopy(candidate)
12
+ scored["idx"] = layer.get("idx")
13
+ scored["key"] = layer.get("key")
14
+ scored["params"] = params
15
+ scored["activation_weight"] = activation_weight
16
+ scored["weighted_mse"] = float(candidate["mse"]) * activation_weight
17
+ scored["total_bits"] = float(candidate["bpw"]) * params
18
+ scored["weighted_sse"] = scored["weighted_mse"] * params
19
+ return scored
20
+
21
+
22
+ def summarize_allocation(selected_layers: Iterable[dict]) -> dict:
23
+ selected = list(selected_layers)
24
+ total_params = sum(int(item["params"]) for item in selected)
25
+ if total_params <= 0:
26
+ raise ValueError("allocation must contain at least one parameter")
27
+
28
+ total_bits = sum(float(item["bpw"]) * int(item["params"]) for item in selected)
29
+ weighted_sse = sum(float(item.get("weighted_mse", item["mse"])) * int(item["params"]) for item in selected)
30
+ weighted_mse = weighted_sse / total_params
31
+ return {
32
+ "layers": len(selected),
33
+ "total_params": total_params,
34
+ "avg_bpw": total_bits / total_params,
35
+ "weighted_mse": weighted_mse,
36
+ "weighted_rmse": sqrt(weighted_mse),
37
+ "compression_vs_bf16": 16.0 / (total_bits / total_params),
38
+ }
39
+
40
+
41
+ def allocate_mixed_budget(layers: list[dict], target_avg_bpw: float) -> dict:
42
+ if target_avg_bpw <= 0:
43
+ raise ValueError("target_avg_bpw must be positive")
44
+ if not layers:
45
+ raise ValueError("layers must not be empty")
46
+
47
+ candidate_layers: list[list[dict]] = []
48
+ for layer in layers:
49
+ candidates = [_candidate_score(layer, item) for item in layer.get("candidates", [])]
50
+ if not candidates:
51
+ raise ValueError(f"layer {layer.get('key', layer.get('idx'))} has no candidates")
52
+ candidates.sort(key=lambda item: (item["bpw"], item["weighted_mse"], item["method"]))
53
+ candidate_layers.append(candidates)
54
+
55
+ selected_indices = [0 for _ in candidate_layers]
56
+ selected = [candidates[0] for candidates in candidate_layers]
57
+ total_params = sum(item["params"] for item in selected)
58
+ budget_bits = target_avg_bpw * total_params
59
+ current_bits = sum(item["total_bits"] for item in selected)
60
+ if current_bits > budget_bits + 1e-9:
61
+ raise ValueError("cheapest candidates exceed target_avg_bpw")
62
+
63
+ while True:
64
+ best_upgrade = None
65
+ for layer_idx, candidates in enumerate(candidate_layers):
66
+ current = selected[layer_idx]
67
+ for candidate_idx, candidate in enumerate(candidates):
68
+ if candidate_idx == selected_indices[layer_idx]:
69
+ continue
70
+ extra_bits = candidate["total_bits"] - current["total_bits"]
71
+ if extra_bits <= 0:
72
+ continue
73
+ if current_bits + extra_bits > budget_bits + 1e-9:
74
+ continue
75
+ error_reduction = current["weighted_sse"] - candidate["weighted_sse"]
76
+ if error_reduction <= 0:
77
+ continue
78
+ score = error_reduction / extra_bits
79
+ contender = (score, error_reduction, -extra_bits, layer_idx, candidate_idx, candidate)
80
+ if best_upgrade is None or contender > best_upgrade:
81
+ best_upgrade = contender
82
+
83
+ if best_upgrade is None:
84
+ break
85
+
86
+ _, _, _, layer_idx, candidate_idx, candidate = best_upgrade
87
+ current_bits += candidate["total_bits"] - selected[layer_idx]["total_bits"]
88
+ selected_indices[layer_idx] = candidate_idx
89
+ selected[layer_idx] = candidate
90
+
91
+ summary = summarize_allocation(selected)
92
+ summary.update(
93
+ {
94
+ "target_avg_bpw": float(target_avg_bpw),
95
+ "selected_layers": selected,
96
+ "method_counts": _method_counts(selected),
97
+ }
98
+ )
99
+ return summary
100
+
101
+
102
+ def _method_counts(selected_layers: Iterable[dict]) -> dict:
103
+ counts: dict[str, int] = {}
104
+ for item in selected_layers:
105
+ method = str(item["method"])
106
+ counts[method] = counts.get(method, 0) + 1
107
+ return dict(sorted(counts.items()))
src/pack_gguf.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import os
3
+ import numpy as np
4
+ from typing import Dict, Optional, List, Tuple
5
+ from dataclasses import dataclass
6
+
7
+ from .gguf_writer import GGUFWriter, GGML_TYPES
8
+
9
+
10
+ @dataclass
11
+ class QuantizedLayer:
12
+ U: torch.Tensor
13
+ S: torch.Tensor
14
+ Vt: torch.Tensor
15
+ U_scale: torch.Tensor
16
+ S_scale: torch.Tensor
17
+ bit_allocations: Tuple[int, int, int]
18
+
19
+
20
+ def create_lowrank_type_defs() -> str:
21
+ return """
22
+ enum ggml_type {
23
+ GGML_TYPE_F32 = 0,
24
+ GGML_TYPE_F16 = 1,
25
+ GGML_TYPE_Q8_0 = 2,
26
+ GGML_TYPE_Q4_0 = 3,
27
+ GGML_TYPE_Q4_1 = 4,
28
+ GGML_TYPE_LOWRANK_UV_0BIT = 100,
29
+ GGML_TYPE_LOWRANK_UV_1BIT = 101,
30
+ GGML_TYPE_LOWRANK_SIGMA_2BIT = 102,
31
+ };
32
+ """
33
+
34
+
35
+ def pack_sub1bit_model(
36
+ factors: Dict[int, Dict],
37
+ output_path: str,
38
+ model_name: str = "llama-2-7b-sub1bit",
39
+ metadata: Optional[Dict] = None
40
+ ):
41
+ writer = GGUFWriter(output_path)
42
+ writer.add_key_value("general.architecture", "llama")
43
+ writer.add_key_value("general.name", model_name)
44
+ writer.add_key_value("general.file_type", "sub1bit")
45
+ if metadata:
46
+ for key, value in metadata.items():
47
+ writer.add_key_value(key, value)
48
+ for layer_idx, layer_data in factors.items():
49
+ writer.add_tensor(
50
+ f"layer.{layer_idx}.U",
51
+ layer_data.get('U_packed', layer_data['U']).cpu().numpy(),
52
+ GGML_TYPES['int8']
53
+ )
54
+ writer.add_tensor(
55
+ f"layer.{layer_idx}.S",
56
+ layer_data['S'].cpu().numpy().astype(np.float16),
57
+ GGML_TYPES['float16']
58
+ )
59
+ writer.add_tensor(
60
+ f"layer.{layer_idx}.Vt",
61
+ layer_data.get('Vt_packed', layer_data['Vt']).cpu().numpy(),
62
+ GGML_TYPES['int8']
63
+ )
64
+ if 'U_scale' in layer_data:
65
+ writer.add_tensor(
66
+ f"layer.{layer_idx}.U_scale",
67
+ np.array([layer_data['U_scale'].item()], dtype=np.float32),
68
+ GGML_TYPES['float32']
69
+ )
70
+ if 'Vt_scale' in layer_data:
71
+ writer.add_tensor(
72
+ f"layer.{layer_idx}.Vt_scale",
73
+ np.array([layer_data['Vt_scale'].item()], dtype=np.float32),
74
+ GGML_TYPES['float32']
75
+ )
76
+ if 'S_scale' in layer_data:
77
+ writer.add_tensor(
78
+ f"layer.{layer_idx}.S_scale",
79
+ np.array([layer_data['S_scale'].item()], dtype=np.float32),
80
+ GGML_TYPES['float32']
81
+ )
82
+ writer.write()
83
+ return os.path.getsize(output_path)
84
+
85
+
86
+ if __name__ == "__main__":
87
+ dummy_factors = {
88
+ 0: {
89
+ 'U': torch.randn(4096, 16),
90
+ 'S': torch.randn(16),
91
+ 'Vt': torch.randn(16, 4096),
92
+ 'rank': 16
93
+ }
94
+ }
95
+ output_path = "C:/Users/Zwmar/projects/sub1quant/quantized/test.gguf"
96
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
97
+ size = pack_sub1bit_model(dummy_factors, output_path)
98
+ print(f"GGUF file created: {size} bytes")
src/quantization.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+
4
+
5
+ def ternary_quantize(x: torch.Tensor):
6
+ scale = x.abs().max()
7
+ if scale == 0:
8
+ scale = 1.0
9
+ normalized = x / scale
10
+ ternary = torch.sign(normalized)
11
+ ternary[ternary == 0] = 1
12
+ return ternary.to(torch.int8), scale
13
+
14
+
15
+ def ternary_pack(t: torch.Tensor) -> torch.Tensor:
16
+ encoded = t.to(torch.int8) + 1
17
+ n = encoded.numel()
18
+ pad = (5 - n % 5) % 5
19
+ if pad:
20
+ encoded = torch.cat([encoded.flatten(), torch.zeros(pad, dtype=torch.int8, device=encoded.device)])
21
+ weights = torch.tensor([81, 27, 9, 3, 1], dtype=torch.int32, device=encoded.device)
22
+ packed = (encoded.reshape(-1, 5).to(torch.int32) * weights).sum(dim=1)
23
+ return packed.to(torch.uint8)
24
+
25
+
26
+ def ternary_unpack(packed: torch.Tensor, original_shape: tuple) -> torch.Tensor:
27
+ weights = torch.tensor([81, 27, 9, 3, 1], dtype=torch.int32, device=packed.device)
28
+ expanded = packed.to(torch.int32).unsqueeze(-1) // weights % 3
29
+ flat = (expanded - 1).flatten()
30
+ return flat[:np.prod(original_shape)].reshape(original_shape).to(torch.int8)
31
+
32
+
33
+ def sigma_quantize(s: torch.Tensor, num_bits: int = 2):
34
+ max_val = s.abs().max()
35
+ if max_val == 0:
36
+ max_val = 1.0
37
+ qmax = 2 ** (num_bits - 1) - 1
38
+ scale = max_val / qmax
39
+ quantized = (s / scale).round().clamp(-qmax, qmax)
40
+ return quantized.to(torch.int8), scale
41
+
42
+
43
+ def sigma_dequantize(q: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
44
+ return q.float() * scale
45
+
46
+
47
+ def quantize_factor(U: torch.Tensor, S: torch.Tensor, Vt: torch.Tensor, sigma_bits: int = 2):
48
+ U_q, U_scale = ternary_quantize(U)
49
+ Vt_q, Vt_scale = ternary_quantize(Vt)
50
+ S_q, S_scale = sigma_quantize(S, sigma_bits)
51
+ return {
52
+ 'U': U_q, 'U_scale': U_scale,
53
+ 'Vt': Vt_q, 'Vt_scale': Vt_scale,
54
+ 'S': S_q, 'S_scale': S_scale,
55
+ }
56
+
57
+
58
+ def pack_factor(data: dict):
59
+ return {
60
+ 'U_packed': ternary_pack(data['U']),
61
+ 'U_scale': data['U_scale'],
62
+ 'Vt_packed': ternary_pack(data['Vt']),
63
+ 'Vt_scale': data['Vt_scale'],
64
+ 'S': data['S'],
65
+ 'S_scale': data['S_scale'],
66
+ 'U_shape': data['U'].shape,
67
+ 'Vt_shape': data['Vt'].shape,
68
+ }
69
+
70
+
71
+ def unpack_factor(data: dict):
72
+ if 'U_packed' in data:
73
+ U = ternary_unpack(data['U_packed'], data['U_shape'])
74
+ Vt = ternary_unpack(data['Vt_packed'], data['Vt_shape'])
75
+ else:
76
+ U = data['U']
77
+ Vt = data['Vt']
78
+ return {
79
+ 'U': U,
80
+ 'U_scale': data['U_scale'],
81
+ 'Vt': Vt,
82
+ 'Vt_scale': data['Vt_scale'],
83
+ 'S': data['S'],
84
+ 'S_scale': data['S_scale'],
85
+ }
86
+
87
+
88
+ def dequantize_factor(data: dict) -> torch.Tensor:
89
+ if 'U_packed' in data:
90
+ U = ternary_unpack(data['U_packed'], data['U_shape']).float()
91
+ Vt = ternary_unpack(data['Vt_packed'], data['Vt_shape']).float()
92
+ else:
93
+ U = data['U'].float()
94
+ Vt = data['Vt'].float()
95
+ S = sigma_dequantize(data['S'], data['S_scale'])
96
+ return torch.matmul(U * S.unsqueeze(0), Vt)
test_perplexity.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Perplexity evaluation for quantized .pt checkpoints.
3
+
4
+ This runs a real transformer forward pass after applying quantized weights.
5
+ For GGUF/GEMV latency checks, use run_gemv_chain.py instead.
6
+ """
7
+
8
+ import argparse
9
+ import gc
10
+ from pathlib import Path
11
+
12
+ import torch
13
+
14
+ from scripts.eval_quantized import apply_quantized_weights, eval_perplexity
15
+
16
+
17
+ def evaluate_quantized_perplexity(
18
+ model_name="models/gemma-4-E2B",
19
+ quantized_path="quantized/gemma-4-E2B-sub1bit.pt",
20
+ wikitext_path="data/wiki.test.txt",
21
+ device=None,
22
+ max_length=512,
23
+ stride=512,
24
+ ):
25
+ """Evaluate WikiText perplexity after applying quantized weights."""
26
+ if device is None:
27
+ device = "cuda" if torch.cuda.is_available() else "cpu"
28
+
29
+ if not Path(wikitext_path).exists():
30
+ raise FileNotFoundError(f"WikiText not found: {wikitext_path}")
31
+
32
+ print("=" * 60)
33
+ print("QUANTIZED MODEL PERPLEXITY EVALUATION")
34
+ print("=" * 60)
35
+ print(f"Device: {device}")
36
+ print(f"Model: {model_name}")
37
+ print(f"Quantized: {quantized_path}")
38
+ print(f"WikiText: {wikitext_path}")
39
+
40
+ print("\n[1] Loading quantized checkpoint...")
41
+ q_data = torch.load(quantized_path, map_location="cpu", weights_only=True)
42
+ quantized = q_data["quantized"]
43
+ print(f" {len(quantized)} quantized entries")
44
+
45
+ print("\n[2] Loading base model...")
46
+ from transformers import AutoModelForCausalLM, AutoTokenizer
47
+
48
+ torch_dtype = torch.float16 if device == "cuda" else torch.float32
49
+ tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
50
+ model = AutoModelForCausalLM.from_pretrained(
51
+ model_name,
52
+ device_map=device,
53
+ torch_dtype=torch_dtype,
54
+ trust_remote_code=True,
55
+ )
56
+ model.eval()
57
+
58
+ print("\n[3] Applying quantized weights...")
59
+ apply_stats = apply_quantized_weights(
60
+ model,
61
+ quantized,
62
+ device=device,
63
+ model_dir=model_name if Path(model_name).is_dir() else None,
64
+ checkpoint_weight_keys=q_data.get("weight_keys"),
65
+ )
66
+ print(f" Replaced {apply_stats['replaced']}/{len(quantized)} weights")
67
+ if apply_stats["skipped"]:
68
+ print(f" Skipped {len(apply_stats['skipped'])} shared-KV checkpoint entries")
69
+
70
+ print("\n[4] Evaluating perplexity...")
71
+ ppl, stats = eval_perplexity(
72
+ model,
73
+ tokenizer,
74
+ wikitext_path,
75
+ device,
76
+ max_length=max_length,
77
+ stride=stride,
78
+ )
79
+
80
+ print()
81
+ print("=" * 60)
82
+ print("RESULTS")
83
+ print("=" * 60)
84
+ print(f" Perplexity: {ppl:.4f}")
85
+ print(f" Chunks: {stats['n_chunks']}")
86
+ print(f" Target: <= 10.5")
87
+ print(f" Status: {'PASS' if ppl <= 10.5 else 'FAIL'}")
88
+ print("=" * 60)
89
+
90
+ del model
91
+ gc.collect()
92
+ if device == "cuda":
93
+ torch.cuda.empty_cache()
94
+
95
+ return ppl, stats
96
+
97
+
98
+ if __name__ == "__main__":
99
+ parser = argparse.ArgumentParser(description="Evaluate quantized model perplexity")
100
+ parser.add_argument("--model", default="models/gemma-4-E2B")
101
+ parser.add_argument("--quantized", default="quantized/gemma-4-E2B-sub1bit.pt")
102
+ parser.add_argument("--wikitext", default="data/wiki.test.txt")
103
+ parser.add_argument("--device", default=None)
104
+ parser.add_argument("--max-length", type=int, default=512)
105
+ parser.add_argument("--stride", type=int, default=512)
106
+ args = parser.parse_args()
107
+
108
+ evaluate_quantized_perplexity(
109
+ model_name=args.model,
110
+ quantized_path=args.quantized,
111
+ wikitext_path=args.wikitext,
112
+ device=args.device,
113
+ max_length=args.max_length,
114
+ stride=args.stride,
115
+ )