Instructions to use toxzak/gemma4-e2b-exp-quant with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use toxzak/gemma4-e2b-exp-quant with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="toxzak/gemma4-e2b-exp-quant")# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("toxzak/gemma4-e2b-exp-quant", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use toxzak/gemma4-e2b-exp-quant with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "toxzak/gemma4-e2b-exp-quant" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "toxzak/gemma4-e2b-exp-quant", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/toxzak/gemma4-e2b-exp-quant
- SGLang
How to use toxzak/gemma4-e2b-exp-quant with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "toxzak/gemma4-e2b-exp-quant" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "toxzak/gemma4-e2b-exp-quant", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "toxzak/gemma4-e2b-exp-quant" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "toxzak/gemma4-e2b-exp-quant", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use toxzak/gemma4-e2b-exp-quant with Docker Model Runner:
docker model run hf.co/toxzak/gemma4-e2b-exp-quant
Initial upload: mixed-budget sub-4-bit artifacts + perplexity result
Browse files- README.md +60 -0
- data/wiki.test.txt +0 -0
- eval_results/mixed_budget_full_g128_target4p0_ppl_colab.json +20 -0
- quantized/gemma_mixed_budget_full_g128_target4p0.pt +3 -0
- scripts/eval_quantized.py +383 -0
- scripts/limited_ppl_bench.py +134 -0
- scripts/quantize_mixed_budget.py +214 -0
- src/Sub1BitLLM.py +202 -0
- src/__init__.py +63 -0
- src/error_budget_residual.py +397 -0
- src/gguf_writer.py +95 -0
- src/groupwise_int4.py +128 -0
- src/lowrank_factorization.py +85 -0
- src/mixed_budget.py +107 -0
- src/pack_gguf.py +98 -0
- src/quantization.py +96 -0
- test_perplexity.py +115 -0
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 |
+
)
|