Upload 17 files
Browse files- README.md +131 -3
- RESULTS.md +40 -0
- pyproject.toml +23 -0
- shapequant/__init__.py +9 -0
- shapequant/arch.py +98 -0
- shapequant/cli.py +48 -0
- shapequant/core.py +116 -0
- shapequant/evaluate.py +33 -0
- shapequant/io.py +113 -0
- shapequant/memory.py +45 -0
- shapequant/qlinear.py +114 -0
- shapequant/quantize.py +214 -0
- shapequant/sigma.py +112 -0
- tests/real_models.py +22 -0
- tests/scale_70b.py +35 -0
- tests/test_core.py +64 -0
- tests/test_end_to_end.py +55 -0
README.md
CHANGED
|
@@ -1,3 +1,131 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# shapequant
|
| 2 |
+
|
| 3 |
+
Data-free, training-free quantization of Hugging Face causal language models.
|
| 4 |
+
|
| 5 |
+
No calibration text and no fine-tuning are used. The method builds each layer's input covariance from the weights
|
| 6 |
+
themselves and uses it to place rounding error where the inputs don't reach (noise-shaped rounding). The output is a
|
| 7 |
+
standard **GPTQ-format** checkpoint (INT4/INT8, asymmetric, group-wise), so GPTQ-capable stacks can serve it.
|
| 8 |
+
A dependency-light loader is included.
|
| 9 |
+
|
| 10 |
+
Supported `model_type`s:
|
| 11 |
+
* `llama`, `mistral`, `qwen2`: includes Llama-2/3/3.1/3.3-70B and Qwen2.5-72B.
|
| 12 |
+
* `opt` (pre-LN), `gpt2`.
|
| 13 |
+
|
| 14 |
+
## Install
|
| 15 |
+
|
| 16 |
+
```bash
|
| 17 |
+
pip install -e . # torch, transformers, accelerate, safetensors, huggingface_hub
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
## Python API
|
| 21 |
+
|
| 22 |
+
```python
|
| 23 |
+
from shapequant import quantize_model, load_quantized, perplexity, wikitext2_windows, estimate_memory
|
| 24 |
+
|
| 25 |
+
# uniform INT4, groups of 128 (default method: data-free shaped rounding)
|
| 26 |
+
quantize_model("meta-llama/Llama-3.3-70B-Instruct", "llama70b-sq-int4", bits=4, group_size=128, device="cuda")
|
| 27 |
+
|
| 28 |
+
# mixed INT4/INT8/FP16 at 4.75 bits/weight (type rule: V, attention-output and MLP-output projections weighted 4x)
|
| 29 |
+
quantize_model("HuggingFaceTB/SmolLM2-135M", "smol-sq-mixed", budget=4.75, group_size=64)
|
| 30 |
+
|
| 31 |
+
# baseline on the same grid
|
| 32 |
+
quantize_model("facebook/opt-125m", "opt-rtn-int4", bits=4, method="rtn")
|
| 33 |
+
|
| 34 |
+
model = load_quantized("smol-sq-mixed", device="cpu") # or device_map="auto"
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
Key arguments of `quantize_model(model, out_dir, ...)`:
|
| 38 |
+
|
| 39 |
+
| argument | default | meaning |
|
| 40 |
+
|---|---|---|
|
| 41 |
+
| `bits` | 4 | 4 or 8, uniform |
|
| 42 |
+
| `group_size` | 128 | must divide every input dimension (use 64 for hidden size 576, etc.) |
|
| 43 |
+
| `method` | `"shaped"` | `"shaped"` (data-free covariance) or `"rtn"` (round to nearest, same grid) |
|
| 44 |
+
| `budget` | None | average bits/weight for mixed INT4/INT8/FP16 (must be ≥ INT4 cost, 4 + 32/group) |
|
| 45 |
+
| `lam`, `kappa` | 1.0, 1.0 | design H = I + lam·S; kappa = attention concentration (1 = theory value) |
|
| 46 |
+
| `n_samples` | 16384 | Monte Carlo samples for the MLP-input covariance |
|
| 47 |
+
| `dynamic_groups` | True | fit each group's grid to the error-fed weights (standard GPTQ); False = grids from original weights |
|
| 48 |
+
| `device` | cuda if available | where the per-layer work runs |
|
| 49 |
+
|
| 50 |
+
## CLI
|
| 51 |
+
|
| 52 |
+
```bash
|
| 53 |
+
shapequant memory meta-llama/Llama-3.1-70B # peak-memory estimate from the config only
|
| 54 |
+
shapequant quantize meta-llama/Llama-3.1-70B out/ --bits 4 --group-size 128 --device cuda
|
| 55 |
+
shapequant quantize HuggingFaceTB/SmolLM2-135M out/ --budget 5.0 --group-size 64
|
| 56 |
+
shapequant eval out/ --windows 32 # WikiText-2 perplexity (needs `datasets`) or --text file.txt
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
## How it scales to 70B
|
| 60 |
+
|
| 61 |
+
* **Streaming.** Weights are read tensor by tensor from the safetensors shards. Only one decoder layer is ever
|
| 62 |
+
resident, and it is written out before the next one is read. Host RAM is not proportional to model size.
|
| 63 |
+
* **Per-layer device work.**
|
| 64 |
+
* The residual-stream second moment (d×d) and the norm-output moments are updated layer by layer.
|
| 65 |
+
* Monte Carlo of the layer's own MLP nonlinearity gives the (inter×inter) MLP-input covariance.
|
| 66 |
+
* Error-feedback rounding uses lazy block updates (block 128). Modules that read the same input
|
| 67 |
+
(q/k/v, gate/up) are stacked and rounded in one pass.
|
| 68 |
+
* **Peak device memory** is about one layer in fp32 plus two (in×in) fp32 matrices for the largest input.
|
| 69 |
+
For Llama-3.1-70B the largest input is the 28672-wide MLP input. `shapequant memory <model>` prints the estimate.
|
| 70 |
+
* **Output.** Sharded `model-0000x-of-0000y.safetensors` (4 GB shards by default) + index + `config.json`
|
| 71 |
+
with `quantization_config` (`quant_method: gptq`, `checkpoint_format: gptq`, per-module overrides under `dynamic`).
|
| 72 |
+
|
| 73 |
+
## Checkpoint format
|
| 74 |
+
|
| 75 |
+
The checkpoint is GPTQ v1, as written by AutoGPTQ/GPTQModel:
|
| 76 |
+
* `qweight` int32 (in·b/32, out); `qzeros` int32 (G, out·b/32), stored as z − 1; `scales` fp16 (G, out);
|
| 77 |
+
`g_idx` = arange(in)//group; `sym=False`, `desc_act=False`.
|
| 78 |
+
* Zero-points are kept ≥ 1, so the v1 "z − 1" convention never underflows.
|
| 79 |
+
* Mixed-precision modules are listed in `quantization_config["dynamic"]`:
|
| 80 |
+
* `"+:<regex>": {"bits": 8}` for INT8 modules;
|
| 81 |
+
* `"-:<regex>": {}` for modules left in FP16 (GPTQModel convention).
|
| 82 |
+
* Everything else (embeddings, norms, untied lm_head) is stored in fp16.
|
| 83 |
+
|
| 84 |
+
## Results
|
| 85 |
+
|
| 86 |
+
`RESULTS.md` has the full numbers. Summary (WikiText-2, 32 × 1024 tokens; perplexity, lower is better):
|
| 87 |
+
|
| 88 |
+
| model | dense | RTN INT4 | shaped INT4 | RTN mixed | shaped mixed |
|
| 89 |
+
|---|---|---|---|---|---|
|
| 90 |
+
| GPT-2 124M (g128, mixed 4.75 b/w) | 26.84 | 29.91 | **29.14** | 29.62 | **28.86** |
|
| 91 |
+
| OPT-125m (g128, mixed 4.75 b/w) | 29.27 | 31.98 | **31.39** | 30.71 | **30.44** |
|
| 92 |
+
| SmolLM2-135M (g64, mixed 5.0 b/w) | 15.46 | **19.28** | 19.29 | **17.82** | 17.83 |
|
| 93 |
+
|
| 94 |
+
At standard INT4 the gain over RTN is modest (−2.6%, −1.9%, +0.1%). It was much larger at 3 bits in the research
|
| 95 |
+
experiments, but 3-bit is not a standard packed format and is not exported here.
|
| 96 |
+
Reproduce with `python tests/real_models.py <model> <out> --group 128 --budget 4.75`.
|
| 97 |
+
|
| 98 |
+
## Verified here, and not
|
| 99 |
+
|
| 100 |
+
* **Verified:**
|
| 101 |
+
* unit tests and tiny-model round trips for llama / qwen2 / opt / gpt2;
|
| 102 |
+
* real GPT-2, OPT-125m and SmolLM2-135M runs (quantize → save → load → perplexity) on CPU;
|
| 103 |
+
* memory estimates from the real 70B configs;
|
| 104 |
+
* the attention-sized per-layer steps at full Llama-3.1-70B shapes on CPU.
|
| 105 |
+
* **Not verified:**
|
| 106 |
+
* an end-to-end 70B run (needs a GPU);
|
| 107 |
+
* the MLP-sized 70B steps (need ~7 GB for two 28672² matrices);
|
| 108 |
+
* loading the checkpoints with GPTQModel / vLLM kernels. The format follows the GPTQ v1 layout and the bundled
|
| 109 |
+
loader reads it, but no GPU kernel was run here.
|
| 110 |
+
|
| 111 |
+
## Method
|
| 112 |
+
|
| 113 |
+
For a linear block y = xW with quantization error R, the output error is tr(RᵀΣR) with Σ = E[xxᵀ].
|
| 114 |
+
No data is available, so Σ is built from the model:
|
| 115 |
+
* **LayerNorm/RMSNorm inputs:** token embeddings under a Zipf prior on token id (plus positions), then every
|
| 116 |
+
earlier attention-output and MLP-output projection added to the residual stream.
|
| 117 |
+
* **MLP second input:** the layer's own nonlinearity applied by Monte Carlo to that Gaussian.
|
| 118 |
+
* **Attention output:** a mixture of value vectors, E[ooᵀ] = μμᵀ + κ·blockdiag(C_v).
|
| 119 |
+
|
| 120 |
+
Rounding is the sequential error-feedback recursion that minimises tr(R H Rᵀ) with H = I + λS. With H = I it is
|
| 121 |
+
exactly round-to-nearest.
|
| 122 |
+
|
| 123 |
+
## Tests
|
| 124 |
+
|
| 125 |
+
```bash
|
| 126 |
+
pytest -q tests/test_core.py tests/test_end_to_end.py
|
| 127 |
+
```
|
| 128 |
+
* `test_core.py`: H = I reproduces RTN; blocked == sequential recursion; GPTQ pack/unpack is exact;
|
| 129 |
+
zero-point safety; shaping lowers the H-weighted error.
|
| 130 |
+
* `test_end_to_end.py`: tiny Llama/Qwen2/OPT/GPT-2 models; quantize → save → load gives logits identical to the
|
| 131 |
+
dequantized reference (uniform and mixed bits).
|
RESULTS.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Results
|
| 2 |
+
|
| 3 |
+
CPU (2 cores), WikiText-2 test, 32 windows of 1024 tokens, one run per setting. Final package defaults:
|
| 4 |
+
shaped coding with λ = 1 and κ = 1 (nothing tuned) and dynamic group grids. RTN uses the same GPTQ grid.
|
| 5 |
+
|
| 6 |
+
| model | group | dense | RTN INT4 | shaped INT4 | RTN mixed | shaped mixed | mixed bits/w |
|
| 7 |
+
|---|---|---|---|---|---|---|---|
|
| 8 |
+
| GPT-2 124M | 128 | 26.84 | 29.91 | **29.14** | 29.62 | **28.86** | 4.75 |
|
| 9 |
+
| OPT-125m | 128 | 29.27 | 31.98 | **31.39** | 30.71 | **30.44** | 4.75 |
|
| 10 |
+
| SmolLM2-135M (Llama) | 64 | 15.46 | **19.28** | 19.29 | **17.82** | 17.83 | 5.0 |
|
| 11 |
+
|
| 12 |
+
Uniform INT4 is 4 + 32/group bits per quantized weight: 4.25 for groups of 128, 4.5 for groups of 64.
|
| 13 |
+
"Mixed" is INT4/INT8/FP16 per module chosen by the type rule at the stated budget.
|
| 14 |
+
|
| 15 |
+
Change in perplexity from shaped coding vs RTN:
|
| 16 |
+
* GPT-2 124M: INT4 -2.6%, mixed -2.6%
|
| 17 |
+
* OPT-125m: INT4 -1.9%, mixed -0.9%
|
| 18 |
+
* SmolLM2-135M (Llama): INT4 +0.1%, mixed +0.1%
|
| 19 |
+
|
| 20 |
+
Static group grids (grids fixed from the original weights), for comparison:
|
| 21 |
+
* GPT-2 124M: shaped INT4 29.09, shaped mixed 28.82
|
| 22 |
+
* OPT-125m: shaped INT4 32.03, shaped mixed 31.02
|
| 23 |
+
* SmolLM2-135M (Llama): shaped INT4 19.19, shaped mixed 17.74
|
| 24 |
+
|
| 25 |
+
Exact per-layer output error vs RTN under the true input covariance (OPT-125m, layers 0/4/8, evaluation only):
|
| 26 |
+
geometric-mean ratio 0.868 (data-free H), 0.282 (true covariance). The layer-level gain only partly reaches perplexity.
|
| 27 |
+
|
| 28 |
+
## 70B scaling
|
| 29 |
+
|
| 30 |
+
Estimates from the real configs (`shapequant memory`):
|
| 31 |
+
|
| 32 |
+
| model | params | device peak | host buffer | INT4 checkpoint | largest H |
|
| 33 |
+
|---|---|---|---|---|---|
|
| 34 |
+
| Llama-3.1-70B | 70.6B | 21.2 GB | 5.6 GB | 37.8 GB | 28672 |
|
| 35 |
+
| Qwen2.5-72B | 72.7B | 22.1 GB | 5.6 GB | 39.4 GB | 29568 |
|
| 36 |
+
| Llama-2-70B | 69.0B | 21.2 GB | 5.6 GB | 34.8 GB | 28672 |
|
| 37 |
+
|
| 38 |
+
Per-layer core at Llama-3.1-70B shapes, executed on cpu (2 cores): stacked q/k/v (10240×8192) 26 s, o_proj (8192×8192) 19 s, attention-output covariance 1.7 s.
|
| 39 |
+
The MLP-sized steps (28672-wide covariance, Monte Carlo) need ~7 GB for two 28672² fp32 matrices and were not run
|
| 40 |
+
on this 7.6 GB machine. 70B needs a GPU (≥ 24 GB, 40–80 GB comfortable). Work per layer is the same order as GPTQ's.
|
pyproject.toml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=61"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "shapequant"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "Data-free, training-free noise-shaped quantization of Hugging Face causal LMs (GPTQ-format output)"
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.9"
|
| 11 |
+
license = { text = "Apache-2.0" }
|
| 12 |
+
authors = [{ name = "Angshul Majumdar", email = "angshul@iiitd.ac.in" }]
|
| 13 |
+
dependencies = ["torch>=2.1", "safetensors>=0.4", "transformers>=4.40", "accelerate>=0.26", "huggingface_hub>=0.20"]
|
| 14 |
+
|
| 15 |
+
[project.optional-dependencies]
|
| 16 |
+
eval = ["datasets"]
|
| 17 |
+
test = ["pytest"]
|
| 18 |
+
|
| 19 |
+
[project.scripts]
|
| 20 |
+
shapequant = "shapequant.cli:main"
|
| 21 |
+
|
| 22 |
+
[tool.setuptools]
|
| 23 |
+
packages = ["shapequant"]
|
shapequant/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""shapequant: data-free, training-free noise-shaped quantization of Hugging Face causal LMs."""
|
| 2 |
+
from .quantize import quantize_model, allocate
|
| 3 |
+
from .qlinear import load_quantized, QuantLinear
|
| 4 |
+
from .evaluate import perplexity, wikitext2_windows, windows_from_text
|
| 5 |
+
from .memory import estimate as estimate_memory
|
| 6 |
+
|
| 7 |
+
__version__ = "0.1.0"
|
| 8 |
+
__all__ = ["quantize_model", "allocate", "load_quantized", "QuantLinear", "perplexity", "wikitext2_windows",
|
| 9 |
+
"windows_from_text", "estimate_memory"]
|
shapequant/arch.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Architecture adapters: tensor names and structure for the supported Hugging Face causal LMs.
|
| 2 |
+
|
| 3 |
+
Supported model_type: llama, mistral, qwen2 (incl. Llama-2/3/3.1/3.3-70B, Qwen2.5-72B), opt (pre-LN), gpt2.
|
| 4 |
+
A linear module is described by (module_name, input_key, role):
|
| 5 |
+
input_key in {"ln1", "attn_out", "ln2", "act"} - which activation it reads
|
| 6 |
+
role in {"q","k","v","qkv","o","gate","up","down"}
|
| 7 |
+
Weights are returned in torch Linear layout (out, in); GPT-2 Conv1D weights are transposed on read.
|
| 8 |
+
"""
|
| 9 |
+
from dataclasses import dataclass, field
|
| 10 |
+
from typing import List, Optional, Tuple
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@dataclass
|
| 14 |
+
class Linear:
|
| 15 |
+
name: str # module path, e.g. "model.layers.3.self_attn.q_proj"
|
| 16 |
+
input_key: str
|
| 17 |
+
role: str
|
| 18 |
+
conv1d: bool = False # stored (in, out)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class Arch:
|
| 23 |
+
model_type: str
|
| 24 |
+
n_layer: int
|
| 25 |
+
d: int
|
| 26 |
+
n_head: int
|
| 27 |
+
n_kv: int
|
| 28 |
+
head_dim: int
|
| 29 |
+
inter: int
|
| 30 |
+
norm: str # "rms" | "ln"
|
| 31 |
+
norm_eps: float
|
| 32 |
+
act: str # "swiglu" | "relu" | "gelu_new" | "gelu"
|
| 33 |
+
embed: str
|
| 34 |
+
pos_embed: Optional[str]
|
| 35 |
+
pos_offset: int
|
| 36 |
+
final_norm: str
|
| 37 |
+
lm_head: Optional[str]
|
| 38 |
+
vocab: int
|
| 39 |
+
max_pos: int
|
| 40 |
+
layers: List[List[Linear]] = field(default_factory=list)
|
| 41 |
+
norms: List[Tuple[str, str]] = field(default_factory=list) # per layer: (ln1 prefix, ln2 prefix)
|
| 42 |
+
|
| 43 |
+
def norm_names(self, L, which):
|
| 44 |
+
p = self.norms[L][0 if which == "ln1" else 1]
|
| 45 |
+
return p + ".weight", (p + ".bias" if self.norm == "ln" else None)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def from_config(cfg: dict) -> Arch:
|
| 49 |
+
mt = cfg.get("model_type")
|
| 50 |
+
if mt in ("llama", "mistral", "qwen2"):
|
| 51 |
+
d, H = cfg["hidden_size"], cfg["num_attention_heads"]
|
| 52 |
+
act = cfg.get("hidden_act", "silu")
|
| 53 |
+
if act != "silu":
|
| 54 |
+
raise NotImplementedError(f"{mt} with hidden_act={act}")
|
| 55 |
+
a = Arch(mt, cfg["num_hidden_layers"], d, H, cfg.get("num_key_value_heads", H), cfg.get("head_dim") or d // H,
|
| 56 |
+
cfg["intermediate_size"], "rms", cfg.get("rms_norm_eps", 1e-6), "swiglu",
|
| 57 |
+
"model.embed_tokens.weight", None, 0, "model.norm.weight",
|
| 58 |
+
None if cfg.get("tie_word_embeddings", False) else "lm_head.weight",
|
| 59 |
+
cfg["vocab_size"], cfg.get("max_position_embeddings", 4096))
|
| 60 |
+
for L in range(a.n_layer):
|
| 61 |
+
p = f"model.layers.{L}."
|
| 62 |
+
a.layers.append([Linear(p + "self_attn.q_proj", "ln1", "q"), Linear(p + "self_attn.k_proj", "ln1", "k"),
|
| 63 |
+
Linear(p + "self_attn.v_proj", "ln1", "v"), Linear(p + "self_attn.o_proj", "attn_out", "o"),
|
| 64 |
+
Linear(p + "mlp.gate_proj", "ln2", "gate"), Linear(p + "mlp.up_proj", "ln2", "up"),
|
| 65 |
+
Linear(p + "mlp.down_proj", "act", "down")])
|
| 66 |
+
a.norms.append((p + "input_layernorm", p + "post_attention_layernorm"))
|
| 67 |
+
return a
|
| 68 |
+
if mt == "opt":
|
| 69 |
+
if not cfg.get("do_layer_norm_before", True):
|
| 70 |
+
raise NotImplementedError("post-LN OPT (opt-350m) is not supported")
|
| 71 |
+
if cfg.get("word_embed_proj_dim", cfg["hidden_size"]) != cfg["hidden_size"]:
|
| 72 |
+
raise NotImplementedError("OPT with project_in/out is not supported")
|
| 73 |
+
d, H = cfg["hidden_size"], cfg["num_attention_heads"]
|
| 74 |
+
a = Arch(mt, cfg["num_hidden_layers"], d, H, H, d // H, cfg["ffn_dim"], "ln", 1e-5,
|
| 75 |
+
{"relu": "relu"}.get(cfg.get("activation_function", "relu"), cfg.get("activation_function")),
|
| 76 |
+
"model.decoder.embed_tokens.weight", "model.decoder.embed_positions.weight", 2,
|
| 77 |
+
"model.decoder.final_layer_norm.weight", None if cfg.get("tie_word_embeddings", True) else "lm_head.weight",
|
| 78 |
+
cfg["vocab_size"], cfg.get("max_position_embeddings", 2048))
|
| 79 |
+
for L in range(a.n_layer):
|
| 80 |
+
p = f"model.decoder.layers.{L}."
|
| 81 |
+
a.layers.append([Linear(p + "self_attn.q_proj", "ln1", "q"), Linear(p + "self_attn.k_proj", "ln1", "k"),
|
| 82 |
+
Linear(p + "self_attn.v_proj", "ln1", "v"), Linear(p + "self_attn.out_proj", "attn_out", "o"),
|
| 83 |
+
Linear(p + "fc1", "ln2", "up"), Linear(p + "fc2", "act", "down")])
|
| 84 |
+
a.norms.append((p + "self_attn_layer_norm", p + "final_layer_norm"))
|
| 85 |
+
return a
|
| 86 |
+
if mt == "gpt2":
|
| 87 |
+
d, H = cfg["n_embd"], cfg["n_head"]
|
| 88 |
+
a = Arch(mt, cfg["n_layer"], d, H, H, d // H, cfg.get("n_inner") or 4 * d, "ln", cfg.get("layer_norm_epsilon", 1e-5),
|
| 89 |
+
{"gelu_new": "gelu_new"}.get(cfg.get("activation_function", "gelu_new"), cfg.get("activation_function")),
|
| 90 |
+
"transformer.wte.weight", "transformer.wpe.weight", 0, "transformer.ln_f.weight", None,
|
| 91 |
+
cfg["vocab_size"], cfg.get("n_positions", 1024))
|
| 92 |
+
for L in range(a.n_layer):
|
| 93 |
+
p = f"transformer.h.{L}."
|
| 94 |
+
a.layers.append([Linear(p + "attn.c_attn", "ln1", "qkv", True), Linear(p + "attn.c_proj", "attn_out", "o", True),
|
| 95 |
+
Linear(p + "mlp.c_fc", "ln2", "up", True), Linear(p + "mlp.c_proj", "act", "down", True)])
|
| 96 |
+
a.norms.append((p + "ln_1", p + "ln_2"))
|
| 97 |
+
return a
|
| 98 |
+
raise NotImplementedError(f"model_type={mt!r} is not supported (supported: llama, mistral, qwen2, opt, gpt2)")
|
shapequant/cli.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse, json, sys
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def main(argv=None):
|
| 5 |
+
p = argparse.ArgumentParser("shapequant", description="Data-free, training-free quantization of Hugging Face causal LMs")
|
| 6 |
+
sub = p.add_subparsers(dest="cmd", required=True)
|
| 7 |
+
q = sub.add_parser("quantize", help="quantize a model (hub id or local dir) to a GPTQ-format checkpoint")
|
| 8 |
+
q.add_argument("model"); q.add_argument("out")
|
| 9 |
+
q.add_argument("--bits", type=int, default=4, choices=[4, 8])
|
| 10 |
+
q.add_argument("--group-size", type=int, default=128)
|
| 11 |
+
q.add_argument("--method", default="shaped", choices=["shaped", "rtn"])
|
| 12 |
+
q.add_argument("--budget", type=float, default=None, help="average bits/weight for mixed INT4/INT8/FP16 (e.g. 4.75)")
|
| 13 |
+
q.add_argument("--lam", type=float, default=1.0); q.add_argument("--kappa", type=float, default=1.0)
|
| 14 |
+
q.add_argument("--samples", type=int, default=16384); q.add_argument("--device", default=None)
|
| 15 |
+
q.add_argument("--revision", default=None); q.add_argument("--shard-gb", type=float, default=4.0)
|
| 16 |
+
q.add_argument("--static-groups", action="store_true", help="fit group grids to the original weights instead of the error-fed ones")
|
| 17 |
+
e = sub.add_parser("eval", help="perplexity of a quantized checkpoint on WikiText-2 (or a text file)")
|
| 18 |
+
e.add_argument("ckpt"); e.add_argument("--text", default=None); e.add_argument("--seq-len", type=int, default=1024)
|
| 19 |
+
e.add_argument("--windows", type=int, default=32); e.add_argument("--device", default="cpu"); e.add_argument("--bos", action="store_true")
|
| 20 |
+
m = sub.add_parser("memory", help="peak-memory estimate from a config (hub id, dir or config.json)")
|
| 21 |
+
m.add_argument("model"); m.add_argument("--method", default="shaped", choices=["shaped", "rtn"])
|
| 22 |
+
a = p.parse_args(argv)
|
| 23 |
+
if a.cmd == "quantize":
|
| 24 |
+
from .quantize import quantize_model
|
| 25 |
+
s = quantize_model(a.model, a.out, bits=a.bits, group_size=a.group_size, method=a.method, budget=a.budget, lam=a.lam,
|
| 26 |
+
kappa=a.kappa, n_samples=a.samples, device=a.device, revision=a.revision, max_shard_size_gb=a.shard_gb,
|
| 27 |
+
dynamic_groups=not a.static_groups)
|
| 28 |
+
print(json.dumps({k: v for k, v in s.items() if k != "bits_per_module"}, indent=2))
|
| 29 |
+
elif a.cmd == "eval":
|
| 30 |
+
from transformers import AutoTokenizer
|
| 31 |
+
from .qlinear import load_quantized
|
| 32 |
+
from .evaluate import wikitext2_windows, perplexity
|
| 33 |
+
tok = AutoTokenizer.from_pretrained(a.ckpt)
|
| 34 |
+
model = load_quantized(a.ckpt, device=a.device)
|
| 35 |
+
W = wikitext2_windows(tok, a.seq_len, a.windows, a.text)
|
| 36 |
+
print(f"perplexity {perplexity(model, W, tok.bos_token_id if a.bos else None):.3f} on {W.numel()} tokens")
|
| 37 |
+
elif a.cmd == "memory":
|
| 38 |
+
import os
|
| 39 |
+
from .memory import estimate
|
| 40 |
+
src = a.model
|
| 41 |
+
if not os.path.exists(src):
|
| 42 |
+
from huggingface_hub import hf_hub_download
|
| 43 |
+
src = hf_hub_download(a.model, "config.json")
|
| 44 |
+
print(json.dumps(estimate(src, method=a.method), indent=2))
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
if __name__ == "__main__":
|
| 48 |
+
sys.exit(main())
|
shapequant/core.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Quantizer core (torch, CPU or GPU).
|
| 2 |
+
|
| 3 |
+
Grid: standard asymmetric GPTQ grid per (output row, input group): range includes 0,
|
| 4 |
+
scale = (max - min) / (2^b - 1), integer zero-point z = round(-min / scale), q = clamp(round(w/scale) + z, 0, 2^b - 1),
|
| 5 |
+
w_hat = (q - z) * scale. Grids are fixed from the original weights ("static groups").
|
| 6 |
+
Zero-points are kept >= 1 so the checkpoint can use the GPTQ v1 storage convention (stores z - 1).
|
| 7 |
+
|
| 8 |
+
Rounding: sequential error feedback that minimises tr(R H R^T) for a design matrix H (in x in), i.e. the
|
| 9 |
+
OBQ/GPTQ recursion with lazy block updates. H = None gives round-to-nearest. With H = I it is identical to RTN.
|
| 10 |
+
"""
|
| 11 |
+
import torch
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def group_params(W, bits, group):
|
| 15 |
+
"""W: (out, in). Returns scale, zero of shape (out, in // group)."""
|
| 16 |
+
out, inn = W.shape
|
| 17 |
+
assert inn % group == 0, f"input dim {inn} not divisible by group size {group}"
|
| 18 |
+
maxq = 2 ** bits - 1
|
| 19 |
+
Wg = W.reshape(out, inn // group, group)
|
| 20 |
+
xmin = torch.clamp(Wg.amin(-1), max=0.0)
|
| 21 |
+
xmax = torch.clamp(Wg.amax(-1), min=0.0)
|
| 22 |
+
both0 = (xmin == 0) & (xmax == 0)
|
| 23 |
+
xmin, xmax = torch.where(both0, -torch.ones_like(xmin), xmin), torch.where(both0, torch.ones_like(xmax), xmax)
|
| 24 |
+
scale = (xmax - xmin) / maxq
|
| 25 |
+
zero = torch.round(-xmin / scale)
|
| 26 |
+
# keep z >= 1 (GPTQ v1 storage): if the group has no negative part, use z = 1 and rescale so max still fits
|
| 27 |
+
low = zero < 1
|
| 28 |
+
scale = torch.where(low, xmax / (maxq - 1), scale)
|
| 29 |
+
zero = torch.where(low, torch.ones_like(zero), zero)
|
| 30 |
+
zero = torch.clamp(zero, 1, maxq)
|
| 31 |
+
return scale, zero
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _q(w, scale, zero, maxq):
|
| 35 |
+
return torch.clamp(torch.round(w / scale) + zero, 0, maxq)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def quantize(W, bits, group, H=None, block=128, dynamic_groups=True):
|
| 39 |
+
"""W: (out, in) float tensor (torch Linear layout). Returns (q uint8 (out, in), scale (out, G), zero (out, G)).
|
| 40 |
+
H: (in, in) positive-definite design matrix, or None for round-to-nearest.
|
| 41 |
+
dynamic_groups: each group's grid is fitted to the error-fed (updated) weights when the group starts,
|
| 42 |
+
as in standard GPTQ, so feedback never pushes weights outside their grid. False = grids from the original W."""
|
| 43 |
+
W = W.to(torch.float32)
|
| 44 |
+
out, inn = W.shape
|
| 45 |
+
maxq = 2 ** bits - 1
|
| 46 |
+
scale, zero = group_params(W, bits, group)
|
| 47 |
+
gid = torch.arange(inn, device=W.device) // group
|
| 48 |
+
if H is None:
|
| 49 |
+
Q = _q(W, scale[:, gid], zero[:, gid], maxq)
|
| 50 |
+
return Q.to(torch.uint8), scale, zero
|
| 51 |
+
W = W.clone()
|
| 52 |
+
H = H.to(device=W.device, dtype=torch.float32)
|
| 53 |
+
L = torch.linalg.cholesky(H)
|
| 54 |
+
Hinv = torch.cholesky_inverse(L); del L
|
| 55 |
+
U = torch.linalg.cholesky(Hinv, upper=True); del Hinv
|
| 56 |
+
Q = torch.zeros((out, inn), dtype=torch.uint8, device=W.device)
|
| 57 |
+
for i1 in range(0, inn, block):
|
| 58 |
+
i2 = min(i1 + block, inn); n = i2 - i1
|
| 59 |
+
W1 = W[:, i1:i2].clone(); E1 = torch.zeros_like(W1); U1 = U[i1:i2, i1:i2]
|
| 60 |
+
for i in range(n):
|
| 61 |
+
c = i1 + i; g = c // group
|
| 62 |
+
if dynamic_groups and c % group == 0:
|
| 63 |
+
seg = W1[:, i:i + group] if i + group <= n else torch.cat([W1[:, i:], W[:, i2:c + group]], 1)
|
| 64 |
+
sg, zg = group_params(seg, bits, group)
|
| 65 |
+
scale[:, g], zero[:, g] = sg[:, 0], zg[:, 0]
|
| 66 |
+
w = W1[:, i]
|
| 67 |
+
q = _q(w, scale[:, g], zero[:, g], maxq)
|
| 68 |
+
Q[:, c] = q.to(torch.uint8)
|
| 69 |
+
err = (w - (q - zero[:, g]) * scale[:, g]) / U1[i, i]
|
| 70 |
+
W1[:, i:] -= err.unsqueeze(1) * U1[i, i:].unsqueeze(0)
|
| 71 |
+
E1[:, i] = err
|
| 72 |
+
if i2 < inn:
|
| 73 |
+
W[:, i2:] -= E1 @ U[i1:i2, i2:]
|
| 74 |
+
return Q, scale, zero
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def dequantize(Q, scale, zero, group):
|
| 78 |
+
gid = torch.arange(Q.shape[1], device=Q.device) // group
|
| 79 |
+
return (Q.to(torch.float32) - zero[:, gid]) * scale[:, gid]
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
# --------------------------------------------------------------- GPTQ v1 packing (AutoGPTQ / GPTQModel "gptq" format)
|
| 83 |
+
def pack(Q, scale, zero, bits, group):
|
| 84 |
+
"""Returns dict(qweight int32 (in*bits/32, out), qzeros int32 (G, out*bits/32), scales fp16 (G, out), g_idx int32 (in,))."""
|
| 85 |
+
assert bits in (2, 4, 8)
|
| 86 |
+
per = 32 // bits
|
| 87 |
+
out, inn = Q.shape
|
| 88 |
+
q = Q.t().contiguous().to(torch.int64) # (in, out)
|
| 89 |
+
assert inn % per == 0 and out % per == 0
|
| 90 |
+
qw = torch.zeros((inn // per, out), dtype=torch.int64, device=Q.device)
|
| 91 |
+
for k in range(per):
|
| 92 |
+
qw |= q[k::per, :] << (bits * k)
|
| 93 |
+
z = (zero.t().contiguous().to(torch.int64) - 1) # v1 convention: store z - 1
|
| 94 |
+
qz = torch.zeros((z.shape[0], out // per), dtype=torch.int64, device=Q.device)
|
| 95 |
+
for k in range(per):
|
| 96 |
+
qz |= z[:, k::per] << (bits * k)
|
| 97 |
+
to32 = lambda t: (t & 0xFFFFFFFF).to(torch.int64).where(t < 2 ** 31, t - 2 ** 32).to(torch.int32)
|
| 98 |
+
return {"qweight": to32(qw), "qzeros": to32(qz), "scales": scale.t().contiguous().to(torch.float16),
|
| 99 |
+
"g_idx": (torch.arange(inn, device=Q.device) // group).to(torch.int32)}
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def unpack(qweight, qzeros, scales, g_idx, bits):
|
| 103 |
+
"""Inverse of pack: dequantized weight in torch Linear layout (out, in), float32."""
|
| 104 |
+
per = 32 // bits; mask = 2 ** bits - 1
|
| 105 |
+
qw = qweight.to(torch.int64) & 0xFFFFFFFF
|
| 106 |
+
inn = qw.shape[0] * per
|
| 107 |
+
q = torch.empty((inn, qw.shape[1]), dtype=torch.int64, device=qw.device)
|
| 108 |
+
for k in range(per):
|
| 109 |
+
q[k::per] = (qw >> (bits * k)) & mask
|
| 110 |
+
qz = qzeros.to(torch.int64) & 0xFFFFFFFF
|
| 111 |
+
z = torch.empty((qz.shape[0], qz.shape[1] * per), dtype=torch.int64, device=qz.device)
|
| 112 |
+
for k in range(per):
|
| 113 |
+
z[:, k::per] = ((qz >> (bits * k)) & mask) + 1
|
| 114 |
+
g = g_idx.to(torch.int64)
|
| 115 |
+
W = (q.to(torch.float32) - z[g].to(torch.float32)) * scales[g].to(torch.float32) # (in, out)
|
| 116 |
+
return W.t().contiguous()
|
shapequant/evaluate.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Perplexity on fixed windows (same protocol as the paper experiments)."""
|
| 2 |
+
import math
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def windows_from_text(tokenizer, text, seq_len=1024, n=None, offset=0):
|
| 7 |
+
ids = tokenizer(text, add_special_tokens=False, return_tensors="pt").input_ids[0]
|
| 8 |
+
k = len(ids) // seq_len
|
| 9 |
+
W = ids[: k * seq_len].view(k, seq_len)[offset:]
|
| 10 |
+
return W if n is None else W[:n]
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def wikitext2_windows(tokenizer, seq_len=1024, n=None, path=None):
|
| 14 |
+
if path is not None:
|
| 15 |
+
text = open(path).read()
|
| 16 |
+
else:
|
| 17 |
+
from datasets import load_dataset
|
| 18 |
+
text = "\n\n".join(load_dataset("wikitext", "wikitext-2-raw-v1", split="test")["text"])
|
| 19 |
+
return windows_from_text(tokenizer, text, seq_len, n)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@torch.no_grad()
|
| 23 |
+
def perplexity(model, windows, bos_token_id=None, device=None):
|
| 24 |
+
"""windows: LongTensor (n, L). If bos_token_id is set it is prepended to every window and all L tokens are scored."""
|
| 25 |
+
device = device or next(model.parameters()).device
|
| 26 |
+
nll, cnt = 0.0, 0
|
| 27 |
+
for w in windows:
|
| 28 |
+
ids = w if bos_token_id is None else torch.cat([torch.tensor([bos_token_id]), w])
|
| 29 |
+
ids = ids.to(device)[None]
|
| 30 |
+
logits = model(ids).logits[0, :-1].float()
|
| 31 |
+
tgt = ids[0, 1:]
|
| 32 |
+
nll += float(torch.nn.functional.cross_entropy(logits, tgt, reduction="sum")); cnt += len(tgt)
|
| 33 |
+
return math.exp(nll / cnt)
|
shapequant/io.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Lazy weight reading (one tensor at a time) and sharded safetensors writing. Nothing loads the whole model."""
|
| 2 |
+
import os, json, glob, shutil
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def resolve(path_or_repo, revision=None):
|
| 7 |
+
"""Local directory, or a Hugging Face repo id (downloads config, tokenizer and weight files)."""
|
| 8 |
+
if os.path.isdir(path_or_repo):
|
| 9 |
+
return path_or_repo
|
| 10 |
+
from huggingface_hub import snapshot_download
|
| 11 |
+
return snapshot_download(path_or_repo, revision=revision,
|
| 12 |
+
allow_patterns=["*.json", "*.safetensors", "*.model", "*.txt", "*.tiktoken", "*.bin"],
|
| 13 |
+
ignore_patterns=["*onnx*", "*.msgpack", "*.h5", "*consolidated*", "original/*"])
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class Reader:
|
| 17 |
+
"""Reads individual tensors from safetensors shards (or a small pytorch_model.bin) by name.
|
| 18 |
+
Accepts names with or without a leading 'model.' / 'transformer.' prefix."""
|
| 19 |
+
|
| 20 |
+
def __init__(self, path):
|
| 21 |
+
self.path = path
|
| 22 |
+
st = sorted(glob.glob(os.path.join(path, "*.safetensors")))
|
| 23 |
+
self.where, self._bin = {}, None
|
| 24 |
+
if st:
|
| 25 |
+
from safetensors import safe_open
|
| 26 |
+
self._open = {}
|
| 27 |
+
for f in st:
|
| 28 |
+
with safe_open(f, framework="pt") as h:
|
| 29 |
+
for k in h.keys():
|
| 30 |
+
self.where[k] = f
|
| 31 |
+
else:
|
| 32 |
+
bins = sorted(glob.glob(os.path.join(path, "pytorch_model*.bin")))
|
| 33 |
+
if not bins:
|
| 34 |
+
raise FileNotFoundError(f"no *.safetensors or pytorch_model*.bin in {path}")
|
| 35 |
+
self._bin = {}
|
| 36 |
+
for f in bins:
|
| 37 |
+
sd = torch.load(f, map_location="cpu", mmap=True, weights_only=True)
|
| 38 |
+
for k, v in sd.items():
|
| 39 |
+
self._bin[k] = v; self.where[k] = f
|
| 40 |
+
|
| 41 |
+
def _key(self, name):
|
| 42 |
+
if name in self.where:
|
| 43 |
+
return name
|
| 44 |
+
for cand in (name.split(".", 1)[1] if "." in name else name, "model." + name, "transformer." + name):
|
| 45 |
+
if cand in self.where:
|
| 46 |
+
return cand
|
| 47 |
+
tail = name.split(".", 1)[1] if name.startswith(("model.", "transformer.")) else name
|
| 48 |
+
for k in self.where:
|
| 49 |
+
if k.endswith(tail) and (len(k) == len(tail) or k[-len(tail) - 1] == "."):
|
| 50 |
+
return k
|
| 51 |
+
return None
|
| 52 |
+
|
| 53 |
+
def has(self, name):
|
| 54 |
+
return self._key(name) is not None
|
| 55 |
+
|
| 56 |
+
def get(self, name, device="cpu", dtype=torch.float32):
|
| 57 |
+
k = self._key(name)
|
| 58 |
+
if k is None:
|
| 59 |
+
raise KeyError(name)
|
| 60 |
+
if self._bin is not None:
|
| 61 |
+
t = self._bin[k]
|
| 62 |
+
else:
|
| 63 |
+
from safetensors import safe_open
|
| 64 |
+
with safe_open(self.where[k], framework="pt", device="cpu") as h:
|
| 65 |
+
t = h.get_tensor(k)
|
| 66 |
+
return t.to(device=device, dtype=dtype)
|
| 67 |
+
|
| 68 |
+
def all_keys(self):
|
| 69 |
+
return list(self.where)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class ShardWriter:
|
| 73 |
+
"""Accumulates tensors and writes model-0000x-of-0000y.safetensors shards plus the index json."""
|
| 74 |
+
|
| 75 |
+
def __init__(self, out_dir, max_shard_bytes=4 * 2 ** 30):
|
| 76 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 77 |
+
self.out, self.max, self.buf, self.size, self.shards, self.index = out_dir, max_shard_bytes, {}, 0, [], {}
|
| 78 |
+
|
| 79 |
+
def add(self, name, t):
|
| 80 |
+
t = t.detach().to("cpu").contiguous().clone()
|
| 81 |
+
nb = t.numel() * t.element_size()
|
| 82 |
+
if self.buf and self.size + nb > self.max:
|
| 83 |
+
self.flush()
|
| 84 |
+
self.buf[name] = t; self.size += nb
|
| 85 |
+
|
| 86 |
+
def flush(self):
|
| 87 |
+
if not self.buf:
|
| 88 |
+
return
|
| 89 |
+
from safetensors.torch import save_file
|
| 90 |
+
fn = f"tmp-{len(self.shards):05d}.safetensors"
|
| 91 |
+
save_file(self.buf, os.path.join(self.out, fn), metadata={"format": "pt"})
|
| 92 |
+
self.shards.append((fn, list(self.buf)))
|
| 93 |
+
self.buf, self.size = {}, 0
|
| 94 |
+
|
| 95 |
+
def close(self):
|
| 96 |
+
self.flush()
|
| 97 |
+
n = len(self.shards); total = 0; wm = {}
|
| 98 |
+
for i, (fn, names) in enumerate(self.shards):
|
| 99 |
+
final = "model.safetensors" if n == 1 else f"model-{i + 1:05d}-of-{n:05d}.safetensors"
|
| 100 |
+
os.replace(os.path.join(self.out, fn), os.path.join(self.out, final))
|
| 101 |
+
total += os.path.getsize(os.path.join(self.out, final))
|
| 102 |
+
for k in names:
|
| 103 |
+
wm[k] = final
|
| 104 |
+
if n > 1:
|
| 105 |
+
json.dump({"metadata": {"total_size": total}, "weight_map": wm},
|
| 106 |
+
open(os.path.join(self.out, "model.safetensors.index.json"), "w"), indent=2)
|
| 107 |
+
return total
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def copy_side_files(src, dst):
|
| 111 |
+
for f in os.listdir(src):
|
| 112 |
+
if f.startswith(("tokenizer", "special_tokens_map", "generation_config", "vocab", "merges")) or f.endswith((".model", ".tiktoken")):
|
| 113 |
+
shutil.copy(os.path.join(src, f), os.path.join(dst, f))
|
shapequant/memory.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Peak-memory estimate for quantize_model from a config (no weights needed)."""
|
| 2 |
+
import json, os
|
| 3 |
+
from .arch import from_config
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def estimate(config, n_samples=16384, batch=2048, method="shaped"):
|
| 7 |
+
"""config: dict, path to config.json, or a directory. Returns bytes for the accelerator and for host RAM."""
|
| 8 |
+
if isinstance(config, str):
|
| 9 |
+
p = os.path.join(config, "config.json") if os.path.isdir(config) else config
|
| 10 |
+
config = json.load(open(p))
|
| 11 |
+
a = from_config(config)
|
| 12 |
+
f32, f64 = 4, 8
|
| 13 |
+
lin = sum(_numel(a, l) for l in a.layers[0]) # one decoder layer, fp32 on device
|
| 14 |
+
ins = {"ln1": a.d, "attn_out": a.n_head * a.head_dim, "ln2": a.d, "act": a.inter}
|
| 15 |
+
# quantizing the largest input group: stacked W (fp32) + H + Cholesky chain (2 alive) + error buffer
|
| 16 |
+
worst = 0
|
| 17 |
+
for key, n in ins.items():
|
| 18 |
+
rows = sum(_out(a, l) for l in a.layers[0] if l.input_key == key)
|
| 19 |
+
worst = max(worst, rows * n * f32 * 2 + 2 * n * n * f32 + rows * 128 * f32 * 2)
|
| 20 |
+
shaped = 0
|
| 21 |
+
if method == "shaped":
|
| 22 |
+
shaped = (3 * a.d * a.d * f64 # residual Sig, moments
|
| 23 |
+
+ 2 * a.inter * a.inter * f32 # S_act accumulator + H
|
| 24 |
+
+ batch * (a.d + 2 * a.inter) * f32 # Monte Carlo batch
|
| 25 |
+
+ 4 * (a.n_head * a.head_dim) ** 2 * f64) # attention-output S
|
| 26 |
+
embed = a.vocab * a.d * f32
|
| 27 |
+
device_peak = lin * f32 + max(worst + shaped, embed + 2 * a.d * a.d * f64)
|
| 28 |
+
n_params = a.n_layer * lin + a.vocab * a.d * (1 if a.lm_head is None else 2)
|
| 29 |
+
out_bytes = a.n_layer * lin * (4.25 / 8) + a.vocab * a.d * 2 * (1 if a.lm_head is None else 2)
|
| 30 |
+
return {"model_type": a.model_type, "params_B": n_params / 1e9, "device_peak_GB": device_peak / 2 ** 30,
|
| 31 |
+
"host_buffer_GB": 4 + lin * 2 / 2 ** 30, "int4_checkpoint_GB": out_bytes / 2 ** 30,
|
| 32 |
+
"largest_H_dim": max(ins.values())}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _out(a, l):
|
| 36 |
+
return {"q": a.n_head * a.head_dim, "k": a.n_kv * a.head_dim, "v": a.n_kv * a.head_dim, "qkv": 3 * a.d,
|
| 37 |
+
"o": a.d, "gate": a.inter, "up": a.inter, "down": a.d}[l.role]
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _in(a, l):
|
| 41 |
+
return {"ln1": a.d, "attn_out": a.n_head * a.head_dim, "ln2": a.d, "act": a.inter}[l.input_key]
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _numel(a, l):
|
| 45 |
+
return _out(a, l) * _in(a, l)
|
shapequant/qlinear.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Loading shapequant / GPTQ-v1 checkpoints with plain transformers + torch (no CUDA kernels needed).
|
| 2 |
+
|
| 3 |
+
The checkpoint is standard GPTQ format, so GPTQ-capable stacks (GPTQModel, vLLM, TGI) can load it with their
|
| 4 |
+
fast kernels. This loader is the dependency-light path: each quantized layer keeps packed int32 weights and
|
| 5 |
+
dequantizes on the fly (optionally caching the fp16 weight).
|
| 6 |
+
"""
|
| 7 |
+
import os, re, json, glob
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn as nn
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
|
| 12 |
+
from . import core
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class QuantLinear(nn.Module):
|
| 16 |
+
def __init__(self, in_features, out_features, bits, group_size, bias, dtype=torch.float16, cache=False):
|
| 17 |
+
super().__init__()
|
| 18 |
+
self.in_features, self.out_features, self.bits, self.group_size, self.cache = in_features, out_features, bits, group_size, cache
|
| 19 |
+
per = 32 // bits; G = in_features // group_size
|
| 20 |
+
self.register_buffer("qweight", torch.zeros((in_features // per, out_features), dtype=torch.int32))
|
| 21 |
+
self.register_buffer("qzeros", torch.zeros((G, out_features // per), dtype=torch.int32))
|
| 22 |
+
self.register_buffer("scales", torch.zeros((G, out_features), dtype=torch.float16))
|
| 23 |
+
self.register_buffer("g_idx", torch.zeros(in_features, dtype=torch.int32))
|
| 24 |
+
self.register_buffer("bias", torch.zeros(out_features, dtype=dtype) if bias else None)
|
| 25 |
+
self._w = None
|
| 26 |
+
|
| 27 |
+
def weight_fp(self, dtype):
|
| 28 |
+
if self._w is not None and self._w.dtype == dtype:
|
| 29 |
+
return self._w
|
| 30 |
+
w = core.unpack(self.qweight, self.qzeros, self.scales, self.g_idx, self.bits).to(dtype)
|
| 31 |
+
if self.cache:
|
| 32 |
+
self._w = w
|
| 33 |
+
return w
|
| 34 |
+
|
| 35 |
+
def forward(self, x):
|
| 36 |
+
b = None if self.bias is None else self.bias.to(x.dtype)
|
| 37 |
+
return F.linear(x, self.weight_fp(x.dtype), b)
|
| 38 |
+
|
| 39 |
+
def extra_repr(self):
|
| 40 |
+
return f"in={self.in_features}, out={self.out_features}, bits={self.bits}, group={self.group_size}"
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _bits_for(name, qc):
|
| 44 |
+
bits = qc.get("bits", 4)
|
| 45 |
+
for pat, ov in (qc.get("dynamic") or {}).items():
|
| 46 |
+
sign, rx = (pat[0], pat[2:]) if pat[:2] in ("+:", "-:") else ("+", pat)
|
| 47 |
+
if re.match(rx, name):
|
| 48 |
+
if sign == "-":
|
| 49 |
+
return None
|
| 50 |
+
bits = ov.get("bits", bits)
|
| 51 |
+
return bits
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _set_module(model, name, mod):
|
| 55 |
+
parent, _, child = name.rpartition(".")
|
| 56 |
+
setattr(model.get_submodule(parent) if parent else model, child, mod)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def load_quantized(path, device="cpu", dtype=torch.float16, cache_weights=False, device_map=None):
|
| 60 |
+
"""Load a checkpoint written by quantize_model. device_map="auto" dispatches layers over GPUs/CPU (accelerate)."""
|
| 61 |
+
from transformers import AutoConfig, AutoModelForCausalLM
|
| 62 |
+
from accelerate import init_empty_weights
|
| 63 |
+
from safetensors.torch import load_file
|
| 64 |
+
cfg = AutoConfig.from_pretrained(path)
|
| 65 |
+
qc = cfg.quantization_config if isinstance(cfg.quantization_config, dict) else cfg.quantization_config.to_dict()
|
| 66 |
+
del cfg.quantization_config
|
| 67 |
+
with init_empty_weights(include_buffers=False):
|
| 68 |
+
try:
|
| 69 |
+
model = AutoModelForCausalLM.from_config(cfg, dtype=dtype) # transformers >= 4.56
|
| 70 |
+
except TypeError:
|
| 71 |
+
model = AutoModelForCausalLM.from_config(cfg, torch_dtype=dtype)
|
| 72 |
+
files = sorted(glob.glob(os.path.join(path, "*.safetensors")))
|
| 73 |
+
sd = {}
|
| 74 |
+
for f in files:
|
| 75 |
+
sd.update(load_file(f))
|
| 76 |
+
qnames = sorted({k[: -len(".qweight")] for k in sd if k.endswith(".qweight")})
|
| 77 |
+
msd = model.state_dict()
|
| 78 |
+
def find(name): # map checkpoint module name to model module name (prefix differences)
|
| 79 |
+
for cand in (name, name.split(".", 1)[1], "model." + name):
|
| 80 |
+
try:
|
| 81 |
+
model.get_submodule(cand); return cand
|
| 82 |
+
except AttributeError:
|
| 83 |
+
continue
|
| 84 |
+
raise KeyError(name)
|
| 85 |
+
ren = {}
|
| 86 |
+
for qn in qnames:
|
| 87 |
+
mn = find(qn); old = model.get_submodule(mn)
|
| 88 |
+
if hasattr(old, "in_features"):
|
| 89 |
+
inn, out = old.in_features, old.out_features
|
| 90 |
+
else: # GPT-2 Conv1D: weight (in, out)
|
| 91 |
+
inn, out = old.weight.shape
|
| 92 |
+
b = _bits_for(qn, qc)
|
| 93 |
+
_set_module(model, mn, QuantLinear(inn, out, b, qc["group_size"], (qn + ".bias") in sd, dtype, cache_weights))
|
| 94 |
+
ren[qn] = mn
|
| 95 |
+
fixed = {}
|
| 96 |
+
for k, v in sd.items():
|
| 97 |
+
mod = k.rsplit(".", 1)[0]
|
| 98 |
+
if mod in ren:
|
| 99 |
+
k = ren[mod] + "." + k.rsplit(".", 1)[1]
|
| 100 |
+
elif k not in msd:
|
| 101 |
+
for cand in (k.split(".", 1)[1], "model." + k):
|
| 102 |
+
if cand in msd:
|
| 103 |
+
k = cand; break
|
| 104 |
+
fixed[k] = v.to(dtype) if v.is_floating_point() and not k.endswith(".scales") else v
|
| 105 |
+
missing, unexpected = model.load_state_dict(fixed, strict=False, assign=True)
|
| 106 |
+
model.tie_weights()
|
| 107 |
+
still = [n for n, p in model.named_parameters() if p.device.type == "meta"]
|
| 108 |
+
if still:
|
| 109 |
+
raise RuntimeError(f"weights missing from checkpoint: {still[:5]}")
|
| 110 |
+
model.eval()
|
| 111 |
+
if device_map is not None:
|
| 112 |
+
from accelerate import dispatch_model, infer_auto_device_map
|
| 113 |
+
return dispatch_model(model, infer_auto_device_map(model, no_split_module_classes=model._no_split_modules))
|
| 114 |
+
return model.to(device)
|
shapequant/quantize.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Data-free, training-free quantization of Hugging Face causal LMs, streamed one decoder layer at a time.
|
| 2 |
+
|
| 3 |
+
quantize_model("meta-llama/Llama-3.3-70B-Instruct", "out/llama70b-sq-int4", bits=4, group_size=128)
|
| 4 |
+
|
| 5 |
+
method="shaped": noise-shaped rounding with the model-implied input covariance H = I + lam*S (default, lam = 1).
|
| 6 |
+
method="rtn": plain round-to-nearest on the same grid (baseline).
|
| 7 |
+
budget=<bits/weight>: mixed INT4 / INT8 / FP16 per module by the type rule (writers into the residual stream,
|
| 8 |
+
i.e. V, attention-output and MLP-output projections, count 4x). None = uniform `bits`.
|
| 9 |
+
Output: GPTQ-format checkpoint (quant_method "gptq", checkpoint_format "gptq", sym=False, desc_act=False),
|
| 10 |
+
per-module bit overrides in quantization_config["dynamic"], plus everything else unquantized in fp16.
|
| 11 |
+
"""
|
| 12 |
+
import os, json, time, math
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
from . import core, sigma
|
| 16 |
+
from .arch import from_config
|
| 17 |
+
from .io import resolve, Reader, ShardWriter, copy_side_files
|
| 18 |
+
|
| 19 |
+
__all__ = ["quantize_model", "allocate"]
|
| 20 |
+
|
| 21 |
+
TYPE_WEIGHT = {"q": 1.0, "k": 1.0, "qkv": 1.0, "gate": 1.0, "up": 1.0, "v": 4.0, "o": 4.0, "down": 4.0}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _get_linear(reader, lin, device):
|
| 25 |
+
W = reader.get(lin.name + ".weight", device=device)
|
| 26 |
+
if lin.conv1d:
|
| 27 |
+
W = W.T.contiguous()
|
| 28 |
+
b = reader.get(lin.name + ".bias", device=device) if reader.has(lin.name + ".bias") else None
|
| 29 |
+
return W, b
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _default_device(device):
|
| 33 |
+
if device is not None:
|
| 34 |
+
return torch.device(device)
|
| 35 |
+
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def allocate(arch, reader, budget, group_size, device, log=print):
|
| 39 |
+
"""Per-module bits in {4, 8, 16} minimising sum_l s_l D_l(b_l) under an average bits/weight budget.
|
| 40 |
+
s_l = type weight / ||W_l||_F^2, D_l(b) = ||W_l - RTN_b(W_l)||_F^2 (exact). One streaming pass over the weights."""
|
| 41 |
+
over = 32.0 / group_size
|
| 42 |
+
cost = torch.tensor([4 + over, 8 + over, 16.0], dtype=torch.float64)
|
| 43 |
+
names, D, n, s = [], [], [], []
|
| 44 |
+
for L in range(arch.n_layer):
|
| 45 |
+
for lin in arch.layers[L]:
|
| 46 |
+
W, _ = _get_linear(reader, lin, device)
|
| 47 |
+
row = []
|
| 48 |
+
for b in (4, 8):
|
| 49 |
+
q, sc, z = core.quantize(W, b, group_size)
|
| 50 |
+
row.append(float(((W - core.dequantize(q, sc, z, group_size)) ** 2).sum()))
|
| 51 |
+
row.append(float(((W - W.half().float()) ** 2).sum()))
|
| 52 |
+
names.append(lin.name); D.append(row); n.append(W.numel()); s.append(TYPE_WEIGHT[lin.role] / float((W ** 2).sum()))
|
| 53 |
+
log(f"[allocate] scanned layer {L + 1}/{arch.n_layer}")
|
| 54 |
+
D, n, s = torch.tensor(D, dtype=torch.float64), torch.tensor(n, dtype=torch.float64), torch.tensor(s, dtype=torch.float64)
|
| 55 |
+
N = n.sum()
|
| 56 |
+
used = lambda k: float((n * cost[k]).sum() / N)
|
| 57 |
+
pick = lambda mu: (s[:, None] * D + mu * n[:, None] * cost[None, :]).argmin(1)
|
| 58 |
+
if used(torch.zeros(len(n), dtype=torch.long)) > budget + 1e-9:
|
| 59 |
+
raise ValueError(f"budget {budget} is below the INT4 floor {cost[0]:.3f} bits/weight")
|
| 60 |
+
lo, hi = 0.0, 1.0
|
| 61 |
+
while used(pick(hi)) > budget:
|
| 62 |
+
hi *= 2
|
| 63 |
+
for _ in range(200):
|
| 64 |
+
mid = (lo + hi) / 2
|
| 65 |
+
lo, hi = (mid, hi) if used(pick(mid)) > budget else (lo, mid)
|
| 66 |
+
k = pick(hi)
|
| 67 |
+
while True: # spend leftover budget on the best marginal upgrades
|
| 68 |
+
best, gain = None, 0.0
|
| 69 |
+
for l in range(len(k)):
|
| 70 |
+
if k[l] < 2:
|
| 71 |
+
extra = float(n[l] * (cost[k[l] + 1] - cost[k[l]]) / N)
|
| 72 |
+
if used(k) + extra <= budget + 1e-12:
|
| 73 |
+
r = float(s[l] * (D[l, k[l]] - D[l, k[l] + 1]) / (n[l] * (cost[k[l] + 1] - cost[k[l]])))
|
| 74 |
+
if r > gain:
|
| 75 |
+
best, gain = l, r
|
| 76 |
+
if best is None:
|
| 77 |
+
break
|
| 78 |
+
k[best] += 1
|
| 79 |
+
bits = {nm: (4, 8, 16)[int(x)] for nm, x in zip(names, k)}
|
| 80 |
+
return bits, used(k)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def quantize_model(model, out_dir, bits=4, group_size=128, method="shaped", budget=None, lam=1.0, kappa=1.0,
|
| 84 |
+
n_samples=16384, device=None, max_shard_size_gb=4.0, revision=None, seed=0, dynamic_groups=True, log=print):
|
| 85 |
+
"""Quantize a Hugging Face causal LM (local dir or hub id) and write a GPTQ-format checkpoint to out_dir.
|
| 86 |
+
Peak memory: one decoder layer of weights plus a few (in x in) matrices; see shapequant.memory.estimate."""
|
| 87 |
+
t0 = time.time()
|
| 88 |
+
assert method in ("shaped", "rtn")
|
| 89 |
+
assert bits in (4, 8) or budget is not None
|
| 90 |
+
dev = _default_device(device)
|
| 91 |
+
src = resolve(model, revision)
|
| 92 |
+
cfg = json.load(open(os.path.join(src, "config.json")))
|
| 93 |
+
arch = from_config(cfg)
|
| 94 |
+
reader = Reader(src)
|
| 95 |
+
if budget is not None:
|
| 96 |
+
bits_of, bpw = allocate(arch, reader, budget, group_size, dev, log)
|
| 97 |
+
log(f"[allocate] {sum(v == 4 for v in bits_of.values())} INT4, {sum(v == 8 for v in bits_of.values())} INT8, "
|
| 98 |
+
f"{sum(v == 16 for v in bits_of.values())} FP16 modules; {bpw:.3f} bits/weight")
|
| 99 |
+
else:
|
| 100 |
+
bits_of = None
|
| 101 |
+
|
| 102 |
+
writer = ShardWriter(out_dir, int(max_shard_size_gb * 2 ** 30))
|
| 103 |
+
quantized = set()
|
| 104 |
+
prefixes = tuple(_layer_prefixes(arch))
|
| 105 |
+
for key in reader.all_keys(): # everything outside the decoder layers: fp16 copy
|
| 106 |
+
ck = _canon(key, arch)
|
| 107 |
+
if ck.startswith("lm_head") and arch.lm_head is None: # tied to the input embedding
|
| 108 |
+
continue
|
| 109 |
+
if not ck.startswith(prefixes):
|
| 110 |
+
writer.add(ck, reader.get(key, dtype=torch.float16))
|
| 111 |
+
|
| 112 |
+
res = sigma.ResidualModel(arch, reader, dev) if method == "shaped" else None
|
| 113 |
+
for L in range(arch.n_layer):
|
| 114 |
+
tl = time.time()
|
| 115 |
+
lins = {lin.name: lin for lin in arch.layers[L]}
|
| 116 |
+
W = {nm: _get_linear(reader, lin, dev) for nm, lin in lins.items()}
|
| 117 |
+
S = {}
|
| 118 |
+
if method == "shaped":
|
| 119 |
+
m1 = res.norm_moments(L, "ln1")
|
| 120 |
+
S["ln1"] = sigma.S_norm(m1)
|
| 121 |
+
v = [lin for lin in lins.values() if lin.role in ("v", "qkv")][0]
|
| 122 |
+
Wv, bv = W[v.name]
|
| 123 |
+
if v.role == "qkv":
|
| 124 |
+
Wv, bv = Wv[2 * arch.d:3 * arch.d], (None if bv is None else bv[2 * arch.d:3 * arch.d])
|
| 125 |
+
S["attn_out"] = sigma.S_attn(arch, m1, Wv, bv, kappa)
|
| 126 |
+
o = [lin for lin in lins.values() if lin.role == "o"][0]
|
| 127 |
+
res.add_writer(W[o.name][0])
|
| 128 |
+
m2 = res.norm_moments(L, "ln2")
|
| 129 |
+
S["ln2"] = sigma.S_norm(m2)
|
| 130 |
+
up = [lin for lin in lins.values() if lin.role == "up"][0]
|
| 131 |
+
gate = [lin for lin in lins.values() if lin.role == "gate"]
|
| 132 |
+
S["act"] = sigma.S_act(arch, m2, W[up.name][0], W[up.name][1], W[gate[0].name][0] if gate else None,
|
| 133 |
+
n_samples=n_samples, seed=seed + L, device=dev)
|
| 134 |
+
dn = [lin for lin in lins.values() if lin.role == "down"][0]
|
| 135 |
+
res.add_writer(W[dn.name][0])
|
| 136 |
+
# group modules that read the same input and use the same bit width: one shaped pass on the stacked rows
|
| 137 |
+
groups = {}
|
| 138 |
+
for nm, lin in lins.items():
|
| 139 |
+
b = bits if bits_of is None else bits_of[nm]
|
| 140 |
+
groups.setdefault((lin.input_key, b), []).append(nm)
|
| 141 |
+
uses = {}
|
| 142 |
+
for (ik, b) in groups:
|
| 143 |
+
if b != 16:
|
| 144 |
+
uses[ik] = uses.get(ik, 0) + 1
|
| 145 |
+
for (ik, b), nms in groups.items():
|
| 146 |
+
if b == 16:
|
| 147 |
+
for nm in nms:
|
| 148 |
+
writer.add(nm + ".weight", (W[nm][0].T if lins[nm].conv1d else W[nm][0]).half())
|
| 149 |
+
if W[nm][1] is not None:
|
| 150 |
+
writer.add(nm + ".bias", W[nm][1].half())
|
| 151 |
+
continue
|
| 152 |
+
Wcat = torch.cat([W[nm][0] for nm in nms], 0)
|
| 153 |
+
H = None
|
| 154 |
+
if method == "shaped":
|
| 155 |
+
uses[ik] -= 1 # last use: reuse S's memory for H
|
| 156 |
+
H = S.pop(ik) if uses[ik] == 0 else S[ik].clone()
|
| 157 |
+
H = H.to(device=dev, dtype=torch.float32); H.mul_(lam); H.diagonal().add_(1.0)
|
| 158 |
+
q, sc, z = core.quantize(Wcat, b, group_size, H=H, dynamic_groups=dynamic_groups)
|
| 159 |
+
del H
|
| 160 |
+
r0 = 0
|
| 161 |
+
for nm in nms:
|
| 162 |
+
r1 = r0 + W[nm][0].shape[0]
|
| 163 |
+
pk = core.pack(q[r0:r1], sc[r0:r1], z[r0:r1], b, group_size)
|
| 164 |
+
for k_, t in pk.items():
|
| 165 |
+
writer.add(nm + "." + k_, t)
|
| 166 |
+
if W[nm][1] is not None:
|
| 167 |
+
writer.add(nm + ".bias", W[nm][1].half())
|
| 168 |
+
quantized.add(nm); r0 = r1
|
| 169 |
+
for which in ("ln1", "ln2"):
|
| 170 |
+
gname, bname = arch.norm_names(L, which)
|
| 171 |
+
writer.add(gname, reader.get(gname, dtype=torch.float16))
|
| 172 |
+
if bname and reader.has(bname):
|
| 173 |
+
writer.add(bname, reader.get(bname, dtype=torch.float16))
|
| 174 |
+
del W, S
|
| 175 |
+
if dev.type == "cuda":
|
| 176 |
+
torch.cuda.empty_cache()
|
| 177 |
+
log(f"[quantize] layer {L + 1}/{arch.n_layer} done in {time.time() - tl:.1f}s")
|
| 178 |
+
total = writer.close()
|
| 179 |
+
|
| 180 |
+
qc = {"quant_method": "gptq", "bits": bits if bits_of is None else 4, "group_size": group_size, "desc_act": False,
|
| 181 |
+
"sym": False, "static_groups": not dynamic_groups, "true_sequential": False, "damp_percent": 0.0, "checkpoint_format": "gptq",
|
| 182 |
+
"pack_dtype": "int32", "lm_head": False,
|
| 183 |
+
"quantizer": ["shapequant:0.1.0"],
|
| 184 |
+
"shapequant": {"method": method, "lam": lam, "kappa": kappa, "dynamic_groups": dynamic_groups, "n_samples": n_samples, "budget": budget,
|
| 185 |
+
"data_free": True, "training_free": True}}
|
| 186 |
+
if bits_of is not None:
|
| 187 |
+
qc["dynamic"] = {}
|
| 188 |
+
for nm, b in bits_of.items():
|
| 189 |
+
if b == 8:
|
| 190 |
+
qc["dynamic"]["+:^" + nm.replace(".", r"\.") + "$"] = {"bits": 8}
|
| 191 |
+
elif b == 16:
|
| 192 |
+
qc["dynamic"]["-:^" + nm.replace(".", r"\.") + "$"] = {}
|
| 193 |
+
cfg_out = dict(cfg); cfg_out["quantization_config"] = qc; cfg_out["torch_dtype"] = "float16"
|
| 194 |
+
json.dump(cfg_out, open(os.path.join(out_dir, "config.json"), "w"), indent=2)
|
| 195 |
+
copy_side_files(src, out_dir)
|
| 196 |
+
summary = {"model": model, "out_dir": out_dir, "method": method, "group_size": group_size,
|
| 197 |
+
"bits": bits if bits_of is None else "mixed", "checkpoint_bytes": total, "minutes": (time.time() - t0) / 60}
|
| 198 |
+
if bits_of is not None:
|
| 199 |
+
summary["bits_per_module"] = bits_of
|
| 200 |
+
json.dump(summary, open(os.path.join(out_dir, "shapequant_summary.json"), "w"), indent=2)
|
| 201 |
+
log(f"[done] {out_dir}: {total / 2 ** 30:.2f} GiB in {summary['minutes']:.1f} min")
|
| 202 |
+
return summary
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def _layer_prefixes(arch):
|
| 206 |
+
return [arch.norms[L][0].rsplit(".", 1)[0] + "." for L in range(arch.n_layer)]
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def _canon(key, arch):
|
| 210 |
+
"""Key with the canonical root used by the architecture ('model.' for llama/opt, 'transformer.' for gpt2)."""
|
| 211 |
+
root = "transformer." if arch.model_type == "gpt2" else "model."
|
| 212 |
+
if key.startswith((root, "lm_head")):
|
| 213 |
+
return key
|
| 214 |
+
return root + key
|
shapequant/sigma.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Model-implied input second moments, built from the weights only and streamed layer by layer.
|
| 2 |
+
|
| 3 |
+
Residual stream: Sig_0 = E_{t~pi}[e_t e_t^T] (+ positional embeddings), pi = Zipf prior on token id
|
| 4 |
+
(BPE ids are assigned in merge order, i.e. by frequency); then Sig += W^T W for every writer
|
| 5 |
+
(attention output and MLP output) in order.
|
| 6 |
+
Norm outputs: LayerNorm: C = P Sig P / (tr/n), cov = g C g, mean = beta, M2 = cov + beta beta^T.
|
| 7 |
+
RMSNorm: C = Sig / (tr/n), M2 = g C g, mean = g m / s, cov = M2 - mean mean^T.
|
| 8 |
+
MLP 2nd input: Monte Carlo of the block's own nonlinearity on x ~ N(mean, cov).
|
| 9 |
+
Attention output: E[o o^T] = mu_o mu_o^T + kappa * blockdiag_heads(C_v of the head's KV group).
|
| 10 |
+
Every S is normalised to trace n. Design H = I + lam * S.
|
| 11 |
+
"""
|
| 12 |
+
import math
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
F64 = torch.float64
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ResidualModel:
|
| 19 |
+
def __init__(self, arch, reader, device, ctx=1024, zipf_shift=10.0, chunk=8192):
|
| 20 |
+
self.a, self.r, self.dev = arch, reader, device
|
| 21 |
+
E = reader.get(arch.embed, device=device, dtype=torch.float32)
|
| 22 |
+
V = E.shape[0]
|
| 23 |
+
pi = 1.0 / (torch.arange(V, device=device, dtype=F64) + zipf_shift); pi /= pi.sum()
|
| 24 |
+
Sig = torch.zeros((arch.d, arch.d), dtype=F64, device=device); m = torch.zeros(arch.d, dtype=F64, device=device)
|
| 25 |
+
for s in range(0, V, chunk):
|
| 26 |
+
e = E[s:s + chunk].to(F64); w = pi[s:s + chunk]
|
| 27 |
+
Sig += (e * w[:, None]).T @ e; m += w @ e
|
| 28 |
+
del E
|
| 29 |
+
if arch.pos_embed is not None and reader.has(arch.pos_embed):
|
| 30 |
+
P = reader.get(arch.pos_embed, device=device, dtype=F64)
|
| 31 |
+
T = min(ctx, P.shape[0] - arch.pos_offset)
|
| 32 |
+
P = P[arch.pos_offset: arch.pos_offset + T]
|
| 33 |
+
pm = P.mean(0)
|
| 34 |
+
Sig += P.T @ P / T + torch.outer(m, pm) + torch.outer(pm, m); m = m + pm
|
| 35 |
+
self.Sig, self.m = Sig, m
|
| 36 |
+
|
| 37 |
+
def add_writer(self, W_out_in):
|
| 38 |
+
"""W in torch layout (out=d, in); the writer adds W h with isotropic h."""
|
| 39 |
+
W = W_out_in.to(device=self.dev, dtype=F64)
|
| 40 |
+
self.Sig += W @ W.T
|
| 41 |
+
|
| 42 |
+
def norm_moments(self, L, which):
|
| 43 |
+
a = self.a; n = a.d
|
| 44 |
+
gname, bname = a.norm_names(L, which)
|
| 45 |
+
g = self.r.get(gname, device=self.dev, dtype=F64)
|
| 46 |
+
if a.norm == "ln":
|
| 47 |
+
Sg = self.Sig - self.Sig.mean(0, keepdim=True)
|
| 48 |
+
Sg = Sg - Sg.mean(1, keepdim=True) # P Sig P
|
| 49 |
+
C = Sg / (torch.trace(Sg) / n)
|
| 50 |
+
cov = g[:, None] * C * g[None, :]
|
| 51 |
+
b = self.r.get(bname, device=self.dev, dtype=F64) if bname and self.r.has(bname) else torch.zeros(n, dtype=F64, device=self.dev)
|
| 52 |
+
return b, cov, cov + torch.outer(b, b)
|
| 53 |
+
s2 = torch.trace(self.Sig) / n
|
| 54 |
+
M2 = g[:, None] * (self.Sig / s2) * g[None, :]
|
| 55 |
+
mu = g * self.m / torch.sqrt(s2)
|
| 56 |
+
w, Q = torch.linalg.eigh(M2 - torch.outer(mu, mu))
|
| 57 |
+
cov = (Q * w.clamp(min=0)) @ Q.T
|
| 58 |
+
return mu, cov, M2
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def normalise(M):
|
| 62 |
+
return M.mul_(M.shape[0] / torch.trace(M))
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def S_norm(moments):
|
| 66 |
+
return normalise(moments[2].clone())
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def S_act(arch, moments, W_up, b_up, W_gate, n_samples=16384, batch=2048, seed=0, device="cpu"):
|
| 70 |
+
"""W_up/W_gate: torch layout (inter, d). Returns (inter, inter) float32."""
|
| 71 |
+
mu, cov, _ = moments
|
| 72 |
+
w, Q = torch.linalg.eigh(cov)
|
| 73 |
+
R = (Q * w.clamp(min=0).sqrt()).to(torch.float32)
|
| 74 |
+
mu = mu.to(torch.float32)
|
| 75 |
+
Wu = W_up.to(device=device, dtype=torch.float32).T
|
| 76 |
+
Wg = None if W_gate is None else W_gate.to(device=device, dtype=torch.float32).T
|
| 77 |
+
bu = None if b_up is None else b_up.to(device=device, dtype=torch.float32)
|
| 78 |
+
gen = torch.Generator(device=device).manual_seed(seed)
|
| 79 |
+
M = torch.zeros((Wu.shape[1], Wu.shape[1]), dtype=torch.float32, device=device); done = 0
|
| 80 |
+
while done < n_samples:
|
| 81 |
+
b = min(batch, n_samples - done)
|
| 82 |
+
x = mu + torch.randn((b, R.shape[0]), generator=gen, device=device) @ R.T
|
| 83 |
+
u = x @ Wu
|
| 84 |
+
if bu is not None:
|
| 85 |
+
u = u + bu
|
| 86 |
+
if arch.act == "swiglu":
|
| 87 |
+
z = x @ Wg; h = z * torch.sigmoid(z) * u
|
| 88 |
+
elif arch.act == "relu":
|
| 89 |
+
h = torch.relu(u)
|
| 90 |
+
elif arch.act == "gelu_new":
|
| 91 |
+
h = 0.5 * u * (1 + torch.tanh(math.sqrt(2 / math.pi) * (u + 0.044715 * u ** 3)))
|
| 92 |
+
elif arch.act == "gelu":
|
| 93 |
+
h = torch.nn.functional.gelu(u)
|
| 94 |
+
else:
|
| 95 |
+
raise NotImplementedError(arch.act)
|
| 96 |
+
M += h.T @ h; done += b
|
| 97 |
+
return normalise(M.div_(done))
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def S_attn(arch, moments, W_v, b_v, kappa=1.0):
|
| 101 |
+
"""W_v: torch layout (n_kv*head_dim, d). Returns (n_head*head_dim)^2 float64."""
|
| 102 |
+
mu, cov, _ = moments
|
| 103 |
+
Wv = W_v.to(device=cov.device, dtype=F64)
|
| 104 |
+
mv = Wv @ mu + (0 if b_v is None else b_v.to(device=cov.device, dtype=F64))
|
| 105 |
+
Cv = Wv @ cov @ Wv.T
|
| 106 |
+
H, dh = arch.n_head, arch.head_dim; per = H // arch.n_kv
|
| 107 |
+
n = H * dh
|
| 108 |
+
mo = torch.zeros(n, dtype=F64, device=cov.device); M = torch.zeros((n, n), dtype=F64, device=cov.device)
|
| 109 |
+
for h in range(H):
|
| 110 |
+
gk = h // per; s, t = slice(h * dh, (h + 1) * dh), slice(gk * dh, (gk + 1) * dh)
|
| 111 |
+
mo[s] = mv[t]; M[s, s] = kappa * Cv[t, t]
|
| 112 |
+
return normalise(M + torch.outer(mo, mo))
|
tests/real_models.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Real-model check: quantize -> load -> WikiText-2 perplexity, for RTN and shaped, uniform INT4 and mixed.
|
| 2 |
+
usage: python tests/real_models.py <hf_model_dir_or_id> <out_dir> [--group 128] [--budget 4.75] [--windows 32] [--bos ID] [--text wt2_test.txt]"""
|
| 3 |
+
import argparse, json, os, time, torch
|
| 4 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 5 |
+
from shapequant import quantize_model, load_quantized, perplexity, wikitext2_windows
|
| 6 |
+
|
| 7 |
+
p = argparse.ArgumentParser()
|
| 8 |
+
p.add_argument("model"); p.add_argument("out"); p.add_argument("--group", type=int, default=128)
|
| 9 |
+
p.add_argument("--budget", type=float, default=4.75); p.add_argument("--windows", type=int, default=32)
|
| 10 |
+
p.add_argument("--bos", type=int, default=None); p.add_argument("--text", default=None); p.add_argument("--device", default="cpu")
|
| 11 |
+
a = p.parse_args()
|
| 12 |
+
tok = AutoTokenizer.from_pretrained(a.model)
|
| 13 |
+
W = wikitext2_windows(tok, 1024, a.windows, a.text)
|
| 14 |
+
R = {"dense": perplexity(AutoModelForCausalLM.from_pretrained(a.model, dtype=torch.float32).eval().to(a.device), W, a.bos)}
|
| 15 |
+
for tag, kw in (("rtn_int4", dict(bits=4, method="rtn")), ("shaped_int4", dict(bits=4)),
|
| 16 |
+
("rtn_mixed", dict(budget=a.budget, method="rtn")), ("shaped_mixed", dict(budget=a.budget))):
|
| 17 |
+
t = time.time(); d = os.path.join(a.out, tag)
|
| 18 |
+
s = quantize_model(a.model, d, group_size=a.group, device=a.device, log=lambda *x: None, **kw)
|
| 19 |
+
R[tag] = {"ppl": perplexity(load_quantized(d, dtype=torch.float32, device=a.device), W, a.bos),
|
| 20 |
+
"minutes": (time.time() - t) / 60, "checkpoint_MB": s["checkpoint_bytes"] / 2 ** 20}
|
| 21 |
+
print(tag, R[tag], flush=True)
|
| 22 |
+
json.dump(R, open(os.path.join(a.out, "results.json"), "w"), indent=1)
|
tests/scale_70b.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""70B scaling check without the weights: memory estimates from real configs, and timing of the per-layer core at
|
| 2 |
+
Llama-3.1-70B shapes on whatever device is available (random weights; the arithmetic is shape-dependent only)."""
|
| 3 |
+
import json, sys, time, torch
|
| 4 |
+
from shapequant import estimate_memory, core, sigma
|
| 5 |
+
from shapequant.arch import from_config
|
| 6 |
+
|
| 7 |
+
CFGS = {
|
| 8 |
+
"Llama-3.1-70B": {"model_type": "llama", "hidden_size": 8192, "intermediate_size": 28672, "num_hidden_layers": 80,
|
| 9 |
+
"num_attention_heads": 64, "num_key_value_heads": 8, "vocab_size": 128256, "hidden_act": "silu",
|
| 10 |
+
"rms_norm_eps": 1e-5, "max_position_embeddings": 131072, "tie_word_embeddings": False},
|
| 11 |
+
"Qwen2.5-72B": {"model_type": "qwen2", "hidden_size": 8192, "intermediate_size": 29568, "num_hidden_layers": 80,
|
| 12 |
+
"num_attention_heads": 64, "num_key_value_heads": 8, "vocab_size": 152064, "hidden_act": "silu",
|
| 13 |
+
"rms_norm_eps": 1e-6, "max_position_embeddings": 32768, "tie_word_embeddings": False},
|
| 14 |
+
"Llama-2-70B": {"model_type": "llama", "hidden_size": 8192, "intermediate_size": 28672, "num_hidden_layers": 80,
|
| 15 |
+
"num_attention_heads": 64, "num_key_value_heads": 8, "vocab_size": 32000, "hidden_act": "silu",
|
| 16 |
+
"rms_norm_eps": 1e-5, "max_position_embeddings": 4096, "tie_word_embeddings": False},
|
| 17 |
+
}
|
| 18 |
+
out = {k: estimate_memory(v) for k, v in CFGS.items()}
|
| 19 |
+
print(json.dumps(out, indent=1))
|
| 20 |
+
dev = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 21 |
+
a = from_config(CFGS["Llama-3.1-70B"]); d = a.d; t = {}
|
| 22 |
+
torch.manual_seed(0)
|
| 23 |
+
# attention-input group (q,k,v stacked: 10240 x 8192) and attention-output (8192 x 8192) at full 70B size
|
| 24 |
+
for name, rows in (("qkv_stacked", 64 * 128 + 2 * 8 * 128), ("o_proj", d)):
|
| 25 |
+
W = torch.randn(rows, d, device=dev) * 0.02
|
| 26 |
+
A = torch.randn(d, 256, device=dev) / 16; H = A @ A.T; H.diagonal().add_(1.0)
|
| 27 |
+
t0 = time.time(); q, s, z = core.quantize(W, 4, 128, H=H); t[name] = time.time() - t0
|
| 28 |
+
pk = core.pack(q, s, z, 4, 128); assert pk["qweight"].shape == (d // 8, rows)
|
| 29 |
+
del W, H, A, q, s, z, pk
|
| 30 |
+
# attention-output covariance construction at 70B size (GQA 64 query / 8 KV heads)
|
| 31 |
+
mu = torch.zeros(d, dtype=torch.float64, device=dev); cov = torch.eye(d, dtype=torch.float64, device=dev)
|
| 32 |
+
t0 = time.time(); S = sigma.S_attn(a, (mu, cov, cov), torch.randn(8 * 128, d, device=dev) * 0.02, None); t["S_attn"] = time.time() - t0
|
| 33 |
+
assert S.shape == (d, d); del S, cov
|
| 34 |
+
print(json.dumps({"device": str(dev), "seconds": t}, indent=1))
|
| 35 |
+
json.dump({"estimates": out, "timing": {"device": str(dev), "seconds": t}}, open(sys.argv[1] if len(sys.argv) > 1 else "scale_70b.json", "w"), indent=1)
|
tests/test_core.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from shapequant import core
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def _ref_sequential(W, bits, group, H, dynamic=False):
|
| 6 |
+
"""Unblocked reference of the error-feedback recursion."""
|
| 7 |
+
W = W.clone().float(); out, inn = W.shape; maxq = 2 ** bits - 1
|
| 8 |
+
sc, z = core.group_params(W, bits, group)
|
| 9 |
+
U = torch.linalg.cholesky(torch.cholesky_inverse(torch.linalg.cholesky(H)), upper=True)
|
| 10 |
+
Q = torch.zeros_like(W)
|
| 11 |
+
for c in range(inn):
|
| 12 |
+
g = c // group
|
| 13 |
+
if dynamic and c % group == 0:
|
| 14 |
+
s_, z_ = core.group_params(W[:, c:c + group], bits, group); sc[:, g], z[:, g] = s_[:, 0], z_[:, 0]
|
| 15 |
+
q = torch.clamp(torch.round(W[:, c] / sc[:, g]) + z[:, g], 0, maxq); Q[:, c] = q
|
| 16 |
+
err = (W[:, c] - (q - z[:, g]) * sc[:, g]) / U[c, c]
|
| 17 |
+
W[:, c:] -= err[:, None] * U[c, c:][None, :]
|
| 18 |
+
return Q
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_identity_design_is_rtn():
|
| 22 |
+
torch.manual_seed(0)
|
| 23 |
+
W = torch.randn(64, 256)
|
| 24 |
+
q0, s0, z0 = core.quantize(W, 4, 64)
|
| 25 |
+
for dyn in (False, True):
|
| 26 |
+
q1, s1, z1 = core.quantize(W, 4, 64, H=torch.eye(256), dynamic_groups=dyn)
|
| 27 |
+
assert torch.equal(q0, q1) and torch.equal(s0, s1)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def test_blocked_equals_sequential():
|
| 31 |
+
torch.manual_seed(1)
|
| 32 |
+
W = torch.randn(48, 384); A = torch.randn(384, 384); H = A @ A.T / 384 + torch.eye(384)
|
| 33 |
+
for dyn in (False, True):
|
| 34 |
+
for blk in (32, 64, 128, 256):
|
| 35 |
+
q, _, _ = core.quantize(W, 4, 128, H=H, block=blk, dynamic_groups=dyn)
|
| 36 |
+
assert (q.float() - _ref_sequential(W, 4, 128, H, dyn)).abs().max() == 0, (dyn, blk)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_pack_roundtrip():
|
| 40 |
+
torch.manual_seed(2)
|
| 41 |
+
for bits in (4, 8):
|
| 42 |
+
W = torch.randn(96, 256) * 0.05
|
| 43 |
+
q, s, z = core.quantize(W, bits, 128)
|
| 44 |
+
pk = core.pack(q, s, z, bits, 128)
|
| 45 |
+
Wd = core.unpack(pk["qweight"], pk["qzeros"], pk["scales"], pk["g_idx"], bits)
|
| 46 |
+
ref = core.dequantize(q, s.half().float(), z, 128)
|
| 47 |
+
assert torch.allclose(Wd, ref, atol=0, rtol=0)
|
| 48 |
+
assert pk["qweight"].dtype == torch.int32 and pk["qweight"].shape == (256 * bits // 32, 96)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def test_zero_points_v1_safe():
|
| 52 |
+
W = torch.rand(32, 128) # all positive groups: zero-point must still be >= 1
|
| 53 |
+
q, s, z = core.quantize(W, 4, 64)
|
| 54 |
+
assert z.min() >= 1
|
| 55 |
+
err = (W - core.dequantize(q, s, z, 64)).abs().max() / W.abs().max()
|
| 56 |
+
assert err < 0.05
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def test_shaping_reduces_weighted_error():
|
| 60 |
+
torch.manual_seed(3)
|
| 61 |
+
W = torch.randn(128, 256); A = torch.randn(256, 32); H = torch.eye(256) + A @ A.T
|
| 62 |
+
def cost(q, s, z):
|
| 63 |
+
R = W - core.dequantize(q, s, z, 64); return float(torch.trace(R @ H @ R.T))
|
| 64 |
+
assert cost(*core.quantize(W, 4, 64, H=H)) < cost(*core.quantize(W, 4, 64))
|
tests/test_end_to_end.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tiny random models of every supported architecture: quantize -> save -> load -> logits must equal a model whose
|
| 2 |
+
weights are the dequantized values (exactness of format, loader and module mapping)."""
|
| 3 |
+
import json, os, tempfile
|
| 4 |
+
import pytest
|
| 5 |
+
import torch
|
| 6 |
+
from transformers import AutoModelForCausalLM, LlamaConfig, OPTConfig, GPT2Config, Qwen2Config
|
| 7 |
+
|
| 8 |
+
from shapequant import quantize_model, load_quantized, core
|
| 9 |
+
|
| 10 |
+
CFGS = {
|
| 11 |
+
"llama": LlamaConfig(hidden_size=128, intermediate_size=256, num_hidden_layers=2, num_attention_heads=4,
|
| 12 |
+
num_key_value_heads=2, vocab_size=512, max_position_embeddings=256, tie_word_embeddings=False),
|
| 13 |
+
"qwen2": Qwen2Config(hidden_size=128, intermediate_size=256, num_hidden_layers=2, num_attention_heads=4,
|
| 14 |
+
num_key_value_heads=2, vocab_size=512, max_position_embeddings=256, tie_word_embeddings=True),
|
| 15 |
+
"opt": OPTConfig(hidden_size=128, ffn_dim=256, num_hidden_layers=2, num_attention_heads=4, vocab_size=512,
|
| 16 |
+
max_position_embeddings=256, word_embed_proj_dim=128),
|
| 17 |
+
"gpt2": GPT2Config(n_embd=128, n_layer=2, n_head=4, vocab_size=512, n_positions=256),
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@pytest.mark.parametrize("kind", list(CFGS))
|
| 22 |
+
@pytest.mark.parametrize("budget", [None, 6.5])
|
| 23 |
+
def test_roundtrip(kind, budget):
|
| 24 |
+
torch.manual_seed(0)
|
| 25 |
+
src = AutoModelForCausalLM.from_config(CFGS[kind]).eval()
|
| 26 |
+
with tempfile.TemporaryDirectory() as d:
|
| 27 |
+
src.save_pretrained(os.path.join(d, "src"), safe_serialization=True)
|
| 28 |
+
s = quantize_model(os.path.join(d, "src"), os.path.join(d, "q"), bits=4, group_size=64, budget=budget,
|
| 29 |
+
n_samples=2048, device="cpu", log=lambda *a: None)
|
| 30 |
+
qm = load_quantized(os.path.join(d, "q"), dtype=torch.float32)
|
| 31 |
+
# reference: same architecture, weights replaced by the dequantized values
|
| 32 |
+
ref = AutoModelForCausalLM.from_config(CFGS[kind]).eval()
|
| 33 |
+
ref.load_state_dict(src.state_dict())
|
| 34 |
+
sd = ref.state_dict()
|
| 35 |
+
from safetensors.torch import load_file
|
| 36 |
+
ck = load_file(os.path.join(d, "q", "model.safetensors"))
|
| 37 |
+
qc = json.load(open(os.path.join(d, "q", "config.json")))["quantization_config"]
|
| 38 |
+
from shapequant.qlinear import _bits_for
|
| 39 |
+
n_q = 0
|
| 40 |
+
for k in ck:
|
| 41 |
+
if k.endswith(".qweight"):
|
| 42 |
+
mod = k[:-8]; bits = _bits_for(mod, qc)
|
| 43 |
+
Wd = core.unpack(ck[mod + ".qweight"], ck[mod + ".qzeros"], ck[mod + ".scales"], ck[mod + ".g_idx"], bits)
|
| 44 |
+
key = next(c for c in (mod + ".weight", mod.split(".", 1)[1] + ".weight") if c in sd)
|
| 45 |
+
sd[key] = Wd.T if kind == "gpt2" else Wd
|
| 46 |
+
n_q += 1
|
| 47 |
+
ref.load_state_dict(sd)
|
| 48 |
+
ids = torch.randint(0, 512, (1, 40))
|
| 49 |
+
with torch.no_grad():
|
| 50 |
+
a, b, c = qm(ids).logits, ref(ids).logits, src(ids).logits
|
| 51 |
+
assert n_q > 0
|
| 52 |
+
assert torch.allclose(a, b, atol=2e-3), (a - b).abs().max() # loader == dequantized reference
|
| 53 |
+
assert (a - c).abs().max() < 5 * (c.std() + 1e-3) # close to the original model
|
| 54 |
+
if budget is not None:
|
| 55 |
+
assert "dynamic" in qc and s["bits"] == "mixed"
|