Commit ·
c99f13f
0
Parent(s):
ASHQ1 v6: IQ tier spectrum, --allow-q3-or-lower, has_imatrix safety
Browse files- Full IQ tier range (IQ2_XXS through IQ4_XS) for aggressive spread
- --allow-q3-or-lower now uses IQ types instead of plain Q3_K
- has_imatrix check prevents crash on tensors without imatrix at low bitrates
- Depth factor removed (A/B test proved no PPL benefit)
- IQ4_NL MSE_BPW adjusted for free-upgrade pass
- Ornith-9B-MTP results added to README
- .gitignore +6 -0
- README.md +206 -0
- classifier.py +244 -0
- config_generator.py +155 -0
- constants.py +222 -0
- imatrix_reader.py +179 -0
- main.py +185 -0
- model_reader.py +110 -0
- quantizer.py +75 -0
- requirements.txt +2 -0
- utils.py +53 -0
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.gguf
|
| 2 |
+
*.gguf.hdr
|
| 3 |
+
*.gguf.tmp
|
| 4 |
+
__pycache__/
|
| 5 |
+
*.pyc
|
| 6 |
+
.env
|
README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
pipeline_tag: text-generation
|
| 6 |
+
library_name: gguf
|
| 7 |
+
tags:
|
| 8 |
+
- quantization
|
| 9 |
+
- gguf
|
| 10 |
+
- llama-cpp
|
| 11 |
+
- imatrix
|
| 12 |
+
- hybrid-quantization
|
| 13 |
+
- selective-quantization
|
| 14 |
+
- priority-queue
|
| 15 |
+
- mse
|
| 16 |
+
- theoretical-optimization
|
| 17 |
+
- qwen3.5
|
| 18 |
+
- gemma4
|
| 19 |
+
- moe
|
| 20 |
+
- mtp
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
# ASHQ1 — Autonomous Selective Hybrid Quantization
|
| 24 |
+
|
| 25 |
+
> ⚠️ **Experimental.** ASHQ1 is a personal research project that I will be refining over time. Use at your own risk. Results may vary between architectures and fine-tunes. Feedback and contributions welcome.
|
| 26 |
+
|
| 27 |
+
ASHQ1 is a post-training quantization method for GGUF models that uses an **imatrix-driven priority queue** to maximise theoretical quality per megabyte. Instead of uniform bit-depth or heuristic layer-blocking, it treats tied tensor groups as monolithic entities and greedily upgrades them by strict mathematical utility — the product of summed importance and theoretical MSE reduction, divided by size cost.
|
| 28 |
+
|
| 29 |
+
## Results
|
| 30 |
+
|
| 31 |
+
| Target | Model | Arch | MTP | Actual | PPL (ctx=1024) | vs Q6_K |
|
| 32 |
+
|:------:|-------|:----:|:---:|:------:|:---------------:|:-------:|
|
| 33 |
+
| 5500 | Qwable-9B | Qwen3.5 | — | 5,503 MiB | 7.4334 | — |
|
| 34 |
+
| 5700 | Qwythos-9B | Qwen3.5 | yes | 5,713 MiB | **7.5411** | **−0.047** |
|
| 35 |
+
| 6000 | Ornith-1.0-9B | Qwen3.5 | yes | 6,015 MiB | **7.6774** | — |(1)
|
| 36 |
+
| 6000 | Ornith-1.0-9B | Qwen3.5 | yes | 6,012 MiB | **7.4697** | — |(2)
|
| 37 |
+
|
| 38 |
+
ASHQ1 at 5700 MiB beats uniform Q6_K by **0.047 PPL** at **19% smaller** (5713 vs 7076 MiB).
|
| 39 |
+
|
| 40 |
+
> (1) Using `--allow-q3-or-lower` — aggressive IQ2→Q8_0 spread.
|
| 41 |
+
> (2) Default Q4_K floor — conservative distribution, best PPL at 6000 MiB.
|
| 42 |
+
|
| 43 |
+
## Real-World Validation
|
| 44 |
+
|
| 45 |
+
ASHQ1's theoretical quality advantage transfers to real agentic coding. We tested Ornith-1.0-9B ASHQ1 6500 (6.4 GB, 33% smaller than Q8_0) as the backend for [Pi](https://pi.dev/), an autonomous coding agent that uses `llama.cpp` as its LLM backend.
|
| 46 |
+
|
| 47 |
+
At `temperature 0.6`, the model was tasked with building a complete personal finance dashboard as a single HTML file — Canvas charts, budget tracker, dark mode, transaction filtering, upcoming bills, responsive layout. The agent worked autonomously: planned the architecture, wrote the entire ~1100-line file, caught its own bugs (`date.now` → `date.getTime`), fixed dark mode logic, ran Node.js validation, and iterated until all checks passed. The final `finance-dashboard.html` was a polished, production-quality single-page app — no external dependencies, no hallucinations, no broken features.
|
| 48 |
+
|
| 49 |
+
This is not cherry-picked. It's the first test we ran. The benchmarks didn't lie — ASHQ1 preserves enough quality that a 6.4 GB quant can drive an autonomous coding agent to build complete, working applications from scratch.
|
| 50 |
+
|
| 51 |
+
## How It Works
|
| 52 |
+
|
| 53 |
+
### 1. Floor Assignment
|
| 54 |
+
|
| 55 |
+
Every tensor starts at a minimum tier by class. SSM params and norms lock at F16. Embeddings start at Q5_K. Weight matrices start at Q4_K (or IQ4_XS for QAT models). MTP heads deploy at Q8_0.
|
| 56 |
+
|
| 57 |
+
With `--allow-q3-or-lower`, low-importance tensors (`ffn_down`, `attn_output`, `ssm_out`) start as low as IQ2_XXS, giving the priority queue more room to upgrade important tensors to Q8_0. Tensors missing imatrix data are kept at Q4_K to avoid garbage at low bitrates.
|
| 58 |
+
|
| 59 |
+
### 2. Importance
|
| 60 |
+
|
| 61 |
+
Imatrix `in_sum2` measures how much each weight contributes to the output variance. Layer position weighting was tested but showed no PPL benefit and has been removed.
|
| 62 |
+
|
| 63 |
+
### 3. Tied Group Detection
|
| 64 |
+
|
| 65 |
+
Tensors with numerically identical `in_sum2` arrays are tied (shared weights). They form a single upgrade group — all members upgrade together as one unit. Group importance is the **sum** of its members' importance, preventing large groups from being starved of budget.
|
| 66 |
+
|
| 67 |
+
### 4. Priority Queue Drain
|
| 68 |
+
|
| 69 |
+
All possible single-tier upgrades are pushed into a max-heap:
|
| 70 |
+
|
| 71 |
+
```
|
| 72 |
+
utility/MiB = sum(timp[group]) × (MSE(cur) − MSE(next)) / (size(next) − size(cur))
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
MSE per tier is theoretical: `MSE = 2^(-2 × bpw)`. K-quants get +0.1 effective bpw vs IQ-quants at the same real bpw, so IQ4_NL→Q4_K is a free quality gain.
|
| 76 |
+
|
| 77 |
+
The queue pops the highest-utility upgrade, applies it, pushes the next upgrade for that group, and drains until the budget is exhausted. A final pass catches any remaining zero-cost upgrades.
|
| 78 |
+
|
| 79 |
+
## Why It Works
|
| 80 |
+
|
| 81 |
+
| Problem | ASHQ1 Solution |
|
| 82 |
+
|---------|---------------|
|
| 83 |
+
| Uniform quant wastes bits on low-importance tensors | Priority queue allocates budget where it matters |
|
| 84 |
+
| Heuristic hand-tuning doesn't scale | Single knob: `--size` in MiB |
|
| 85 |
+
| Hand-tuned SHQ hybrids need days of PPL sweeps | Queue converges in ~1 sec for any budget |
|
| 86 |
+
| Large tied groups starved by per-tensor logic | `sum(timp)` prevents 32× group penalty |
|
| 87 |
+
| IQ4_NL→Q4_K at same bpw is a no-op | Free-upgrade pass catches zero-cost quality gains |
|
| 88 |
+
| No PPL-per-budget curve needed | Queue optimises for MSE directly |
|
| 89 |
+
| Tensors without imatrix crash at low bitrates | `has_imatrix` check falls back to Q4_K floor |
|
| 90 |
+
|
| 91 |
+
## Supported Architectures
|
| 92 |
+
|
| 93 |
+
| Arch | Detection | Features |
|
| 94 |
+
|------|-----------|----------|
|
| 95 |
+
| `qwen35` | SSM + QKV | Hybrid attention, SSM layers, GQA, **MTP support** |
|
| 96 |
+
| `mellum2` | MoE (`exps` tensors) | Mixture of Experts, GQA, router F16 |
|
| 97 |
+
| `gemma4` | Layer-scale norms | QAT support, Q4_K attention floor |
|
| 98 |
+
|
| 99 |
+
MTP (Multi-Token Prediction) heads are handled explicitly: MTP tensors deploy at Q8_0 and are excluded from the classifier's budget (their cost is subtracted from the target upfront). Tensor names with `nextn.*` or layers beyond `n_layers` are detected as MTP at runtime.
|
| 100 |
+
|
| 101 |
+
### Looking for: Qwen3.6 support
|
| 102 |
+
|
| 103 |
+
Qwen3.6 is one of the most capable local LLMs right now, but I can't handle it on my hardware. The BF16 source is ~55 GB — I don't have enough RAM to even load it, let alone quantize. If you have access to a Qwen3.6 GGUF (any quantization) and can run `llama-imatrix` on it — or if you'd like to collaborate on adding architecture detection — please reach out. I can handle the integration, I just need the raw tensor names and imatrix data to map out the class system.
|
| 104 |
+
|
| 105 |
+
New architectures can be added via `ARCH_FEATURES` in `constants.py`.
|
| 106 |
+
|
| 107 |
+
## Code Structure
|
| 108 |
+
|
| 109 |
+
| File | Role |
|
| 110 |
+
|------|------|
|
| 111 |
+
| `main.py` | CLI entry point, orchestration, `--show-floors`, multiple `--imatrix` support |
|
| 112 |
+
| `model_reader.py` | Reads GGUF, detects architecture/prefix/n_layers/MTP at runtime |
|
| 113 |
+
| `imatrix_reader.py` | Parses imatrix GGUF, detects tied groups via `np.allclose(in_sum2)`, combines multiple imatrix |
|
| 114 |
+
| `classifier.py` | Floor assignment → tied group building → priority queue drain → free upgrade pass |
|
| 115 |
+
| `config_generator.py` | Generates `--tensor-type` regex rules from classified tensors (valid ECMAScript regex with pipe-alternated ranges) |
|
| 116 |
+
| `quantizer.py` | Subprocess wrapper around `llama-quantize` |
|
| 117 |
+
| `constants.py` | TENSOR_CLASS mapping, CLASS_HARD_FLOORS, CLASS_MAX_TIER, MSE_BPW, TIER_BPW, ARCH_FEATURES |
|
| 118 |
+
|
| 119 |
+
## Usage
|
| 120 |
+
|
| 121 |
+
### Quantization
|
| 122 |
+
|
| 123 |
+
```bash
|
| 124 |
+
pip install -r requirements.txt
|
| 125 |
+
|
| 126 |
+
# Dry run (∼1 sec)
|
| 127 |
+
python main.py --model model.gguf --imatrix imatrix.gguf --size 6800
|
| 128 |
+
|
| 129 |
+
# Actual quant (∼10 min)
|
| 130 |
+
python main.py --model model.gguf --imatrix imatrix.gguf --size 6800 --run
|
| 131 |
+
|
| 132 |
+
# Show hard floors
|
| 133 |
+
python main.py --show-floors
|
| 134 |
+
|
| 135 |
+
# Multiple imatrix (combined with max/mean)
|
| 136 |
+
python main.py --model model.gguf --imatrix i1.gguf --imatrix i2.gguf \
|
| 137 |
+
--imatrix-method max --size 6800 --run
|
| 138 |
+
|
| 139 |
+
# Allow low-bit tensors (IQ2_XXS through Q8_0 spread)
|
| 140 |
+
python main.py --model model.gguf --imatrix imatrix.gguf --size 6000 \
|
| 141 |
+
--allow-q3-or-lower --run
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
The `llama-quantize` binary path is set in `quantizer.py:6`.
|
| 145 |
+
|
| 146 |
+
### Inference (llama-server)
|
| 147 |
+
|
| 148 |
+
Recommended server flags for serving ASHQ1 quants:
|
| 149 |
+
|
| 150 |
+
```bash
|
| 151 |
+
./build/bin/llama-server \
|
| 152 |
+
-m model-ASHQ1.gguf \
|
| 153 |
+
-c 50000 \
|
| 154 |
+
--jinja \
|
| 155 |
+
-fit off \
|
| 156 |
+
-ngl 99 \
|
| 157 |
+
--flash-attn on \
|
| 158 |
+
--cache-type-k q8_0 \
|
| 159 |
+
--cache-type-v q8_0 \
|
| 160 |
+
--port 8080 \
|
| 161 |
+
--mmap \
|
| 162 |
+
--temp 1.0 \
|
| 163 |
+
--top-p 0.95 \
|
| 164 |
+
--min-p 0 \
|
| 165 |
+
--top-k 20 \
|
| 166 |
+
--seed -1 \
|
| 167 |
+
--parallel 1
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
## Tier Reference
|
| 171 |
+
|
| 172 |
+
| Tier | BPW | MSE_BPW |
|
| 173 |
+
|------|:---:|:-------:|
|
| 174 |
+
| F16 | 16.0 | 16.0 |
|
| 175 |
+
| Q8_0 | 8.50 | 8.50 |
|
| 176 |
+
| Q6_K | 6.5625 | 6.5625 |
|
| 177 |
+
| Q5_K | 5.50 | 5.50 |
|
| 178 |
+
| Q4_K | 4.50 | 4.50 |
|
| 179 |
+
| IQ4_NL | 4.50 | (2) |
|
| 180 |
+
| IQ4_XS | 4.25 | 4.25 |
|
| 181 |
+
| Q3_K | 3.4375 | 3.4375 |
|
| 182 |
+
| IQ3_M | 3.66 | — |
|
| 183 |
+
| IQ3_S | 3.44 | 3.44 |
|
| 184 |
+
| IQ3_XXS | 3.0625 | 3.0625 |
|
| 185 |
+
| IQ2_S | 2.50 | 2.50 |
|
| 186 |
+
| IQ2_XS | 2.3125 | 2.3125 |
|
| 187 |
+
| IQ2_XXS | 2.0625 | 2.0625 |
|
| 188 |
+
| IQ1_S | 1.5625 | 1.5625 |
|
| 189 |
+
|
| 190 |
+
> (2) IQ4_NL uses IQ4_XS MSE_BPW for the free-upgrade pass (same real bpw as Q4_K).
|
| 191 |
+
|
| 192 |
+
## Quantization Configs
|
| 193 |
+
|
| 194 |
+
Generated configs are valid `llama-quantize` arguments with ECMAScript-compatible regex patterns. Each `--tensor-type` rule matches a group of tensors that share the same target tier, with layers grouped into contiguous ranges:
|
| 195 |
+
|
| 196 |
+
- `(blk|BLK)\.(3|7|11|15|19|23|27|31)\.attn_k=Q8_0` — specific attention layers at Q8_0
|
| 197 |
+
- `(blk|BLK)\.((?:22|23|24|25|26))\.ffn_gate=Q6_K` — range of FFN layers at Q6_K
|
| 198 |
+
- `.*output_norm.*=F16` — global catch-all
|
| 199 |
+
|
| 200 |
+
Rules are sorted by specificity (specific layers, high tiers first) because `llama-quantize` uses first-match-wins.
|
| 201 |
+
|
| 202 |
+
## References
|
| 203 |
+
|
| 204 |
+
- [ASHQ1 repo](https://huggingface.co/wepiqx/ASHQ1)
|
| 205 |
+
- [GGUF specification](https://github.com/ggerganov/ggml/blob/master/docs/gguf.md)
|
| 206 |
+
- [llama.cpp](https://github.com/ggerganov/llama.cpp)
|
classifier.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import heapq
|
| 2 |
+
import warnings
|
| 3 |
+
from functools import lru_cache
|
| 4 |
+
from typing import Dict, List, Set, Tuple, Any
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
|
| 7 |
+
from constants import (
|
| 8 |
+
TIER_ORDER, TIER_BPW, GGUF_OVERHEAD_FACTOR, CLASS_MAX_TIER,
|
| 9 |
+
CAN_Q3, ALLOW_LOWER_FLOOR, MTP_DEPLOY_TIER, get_tensor_class, get_tensor_type,
|
| 10 |
+
is_mtp_tensor,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
# Выносим делитель в константу (8 бит * 1024 байт * 1024 кбайт)
|
| 14 |
+
BITS_IN_MIB = 8 * 1024 * 1024.0
|
| 15 |
+
|
| 16 |
+
# Предвычисляем множители размеров для каждого тира (ускорение математики)
|
| 17 |
+
TIER_SIZE_MULTIPLIER = {
|
| 18 |
+
tier: (bpw / BITS_IN_MIB) * GGUF_OVERHEAD_FACTOR
|
| 19 |
+
for tier, bpw in TIER_BPW.items()
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
# K-quants используют блоки по 256 элементов и требуют выравнивания (padding) 3D-тензоров MoE
|
| 23 |
+
K_QUANTS = {"Q3_K", "Q4_K", "Q5_K", "Q6_K"}
|
| 24 |
+
MOE_PAD_TYPES = {"ffn_gate_exps", "ffn_up_exps", "ffn_down_exps", "ffn_down"}
|
| 25 |
+
|
| 26 |
+
MSE_BPW = {
|
| 27 |
+
"IQ1_S": 1.5625, "IQ2_XXS": 2.0625, "IQ2_XS": 2.3125,
|
| 28 |
+
"IQ2_S": 2.5,
|
| 29 |
+
"IQ3_XXS": 3.0625,
|
| 30 |
+
"Q3_K": 3.4375, "IQ3_S": 3.44,
|
| 31 |
+
"IQ4_XS": 4.25, "IQ4_NL": 4.25, "Q4_K": 4.50,
|
| 32 |
+
"Q5_K": 5.50, "Q6_K": 6.5625, "Q8_0": 8.50, "F16": 16.0,
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
@dataclass(order=True, slots=True)
|
| 36 |
+
class UpgradeItem:
|
| 37 |
+
"""Элемент очереди апгрейдов. Сравнивается только по neg_utility."""
|
| 38 |
+
neg_utility: float
|
| 39 |
+
group_id: int = field(compare=False)
|
| 40 |
+
next_tier: str = field(compare=False)
|
| 41 |
+
cost_delta: float = field(compare=False)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _tier_index(tier: str) -> int:
|
| 45 |
+
if tier not in TIER_ORDER:
|
| 46 |
+
raise ValueError(f"Unknown tier: {tier}")
|
| 47 |
+
return TIER_ORDER.index(tier)
|
| 48 |
+
|
| 49 |
+
def _tier_at(idx: int) -> str:
|
| 50 |
+
if not (0 <= idx < len(TIER_ORDER)):
|
| 51 |
+
raise IndexError(f"Tier index {idx} out of range")
|
| 52 |
+
return TIER_ORDER[idx]
|
| 53 |
+
|
| 54 |
+
def _size_mib(tier: str, n_elements: int) -> float:
|
| 55 |
+
"""Размер тензора в MiB с учётом оверхеда GGUF."""
|
| 56 |
+
if n_elements <= 0:
|
| 57 |
+
return 0.0
|
| 58 |
+
return n_elements * TIER_SIZE_MULTIPLIER.get(tier, 0.0)
|
| 59 |
+
|
| 60 |
+
@lru_cache(maxsize=64)
|
| 61 |
+
def _mse_delta(cur_tier: str, next_tier: str) -> float:
|
| 62 |
+
return (2 ** (-2 * MSE_BPW[cur_tier])) - (2 ** (-2 * MSE_BPW[next_tier]))
|
| 63 |
+
|
| 64 |
+
def _push_upgrade(group_id: int,
|
| 65 |
+
group_registry: Dict[int, Tuple[List[str], int, int]],
|
| 66 |
+
assignments: Dict[str, str],
|
| 67 |
+
tensor_importance: Dict[str, float],
|
| 68 |
+
upgrade_queue: List[UpgradeItem],
|
| 69 |
+
importance_table: Dict[str, Any]):
|
| 70 |
+
|
| 71 |
+
# Храним 3 элемента: имена, реальный размер, размер с padding
|
| 72 |
+
g_names, g_elements, g_elements_padded = group_registry[group_id]
|
| 73 |
+
rep_name = g_names[0]
|
| 74 |
+
cur_tier = assignments[rep_name]
|
| 75 |
+
cur_idx = _tier_index(cur_tier)
|
| 76 |
+
|
| 77 |
+
rep_info = importance_table.get(rep_name, {})
|
| 78 |
+
ttype = rep_info["type"] if "type" in rep_info else get_tensor_type(rep_name)
|
| 79 |
+
cls = get_tensor_class(ttype)
|
| 80 |
+
max_tier = CLASS_MAX_TIER.get(cls, "Q8_0")
|
| 81 |
+
|
| 82 |
+
if cur_idx >= _tier_index(max_tier) or cur_idx >= len(TIER_ORDER) - 1:
|
| 83 |
+
return
|
| 84 |
+
|
| 85 |
+
next_tier = _tier_at(cur_idx + 1)
|
| 86 |
+
|
| 87 |
+
# Выбираем размер в зависимости от того, относится ли тир к K-quants
|
| 88 |
+
cur_size_g = g_elements_padded if cur_tier in K_QUANTS else g_elements
|
| 89 |
+
next_size_g = g_elements_padded if next_tier in K_QUANTS else g_elements
|
| 90 |
+
|
| 91 |
+
cost_delta = _size_mib(next_tier, next_size_g) - _size_mib(cur_tier, cur_size_g)
|
| 92 |
+
|
| 93 |
+
quality_delta = _mse_delta(cur_tier, next_tier)
|
| 94 |
+
if quality_delta <= 0:
|
| 95 |
+
return
|
| 96 |
+
|
| 97 |
+
if cost_delta == 0:
|
| 98 |
+
utility_per_mb = float('inf')
|
| 99 |
+
elif cost_delta < 0:
|
| 100 |
+
return
|
| 101 |
+
else:
|
| 102 |
+
total_g_imp = sum(tensor_importance.get(n, 0) for n in g_names)
|
| 103 |
+
utility_per_mb = (total_g_imp * quality_delta) / cost_delta
|
| 104 |
+
|
| 105 |
+
heapq.heappush(upgrade_queue, UpgradeItem(-utility_per_mb, group_id, next_tier, cost_delta))
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def compute_initial_assignments(non_mtp_names: Set[str], mtp_names: Set[str],
|
| 109 |
+
importance_table: Dict, allow_q3: bool, is_qat: bool = False) -> Dict[str, str]:
|
| 110 |
+
assignments = {name: MTP_DEPLOY_TIER for name in mtp_names}
|
| 111 |
+
|
| 112 |
+
for name in non_mtp_names:
|
| 113 |
+
rep_info = importance_table.get(name, {})
|
| 114 |
+
has_imatrix = "importance_mean" in rep_info
|
| 115 |
+
ttype = rep_info["type"] if "type" in rep_info else get_tensor_type(name)
|
| 116 |
+
cls = get_tensor_class(ttype)
|
| 117 |
+
|
| 118 |
+
if cls in ("norms", "ssm_params"):
|
| 119 |
+
assignments[name] = "F16"
|
| 120 |
+
elif cls == "embd":
|
| 121 |
+
assignments[name] = "Q4_K" if is_qat else "Q5_K"
|
| 122 |
+
else:
|
| 123 |
+
base_floor = ("Q4_K" if cls == "attn_proj" else "IQ4_XS") if is_qat else "Q4_K"
|
| 124 |
+
if allow_q3 and cls in CAN_Q3 and has_imatrix:
|
| 125 |
+
assignments[name] = ALLOW_LOWER_FLOOR
|
| 126 |
+
else:
|
| 127 |
+
assignments[name] = base_floor
|
| 128 |
+
|
| 129 |
+
return assignments
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def build_groups(tied_groups: List[List[str]], non_mtp_names: Set[str],
|
| 133 |
+
ne_map: Dict[str, int], padded_ne_map: Dict[str, int]) -> Dict[int, Tuple[List[str], int, int]]:
|
| 134 |
+
group_registry = {}
|
| 135 |
+
assigned_tensors = set()
|
| 136 |
+
|
| 137 |
+
for group_idx, group in enumerate(tied_groups):
|
| 138 |
+
clean_group = [n for n in group if n in non_mtp_names]
|
| 139 |
+
if clean_group:
|
| 140 |
+
g_elements = sum(ne_map.get(n, 0) for n in clean_group)
|
| 141 |
+
g_elements_padded = sum(padded_ne_map.get(n, 0) for n in clean_group)
|
| 142 |
+
group_registry[group_idx] = (clean_group, g_elements, g_elements_padded)
|
| 143 |
+
assigned_tensors.update(clean_group)
|
| 144 |
+
|
| 145 |
+
unassigned_tensors = non_mtp_names - assigned_tensors
|
| 146 |
+
next_group_idx = len(group_registry)
|
| 147 |
+
|
| 148 |
+
for name in unassigned_tensors:
|
| 149 |
+
group_registry[next_group_idx] = ([name], ne_map.get(name, 0), padded_ne_map.get(name, 0))
|
| 150 |
+
next_group_idx += 1
|
| 151 |
+
|
| 152 |
+
return group_registry
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def optimal_classify(importance_table: dict, tied_groups: list, model: dict,
|
| 156 |
+
target_size_mib: float, allow_q3: bool = False) -> Tuple[dict, dict]:
|
| 157 |
+
if target_size_mib <= 0:
|
| 158 |
+
raise ValueError("target_size_mib must be positive")
|
| 159 |
+
|
| 160 |
+
features = model.get("features", {})
|
| 161 |
+
has_mtp = features.get("has_mtp", False)
|
| 162 |
+
n_layers = features.get("n_layers", 31)
|
| 163 |
+
is_qat = features.get("is_qat", False)
|
| 164 |
+
model_tensors = model.get("tensors", {})
|
| 165 |
+
|
| 166 |
+
ne_map = {k: v["n_elements"] for k, v in model_tensors.items()}
|
| 167 |
+
for tname, info in importance_table.items():
|
| 168 |
+
if tname not in ne_map:
|
| 169 |
+
ne_map[tname] = info["n_elements"]
|
| 170 |
+
|
| 171 |
+
# --- Вычисление MoE Padding (Целочисленное выравнивание) ---
|
| 172 |
+
moe_d_ff = features.get("moe_intermediate_size", 0)
|
| 173 |
+
padded_ne_map = dict(ne_map)
|
| 174 |
+
if moe_d_ff > 0 and moe_d_ff % 256 != 0:
|
| 175 |
+
aligned_d_ff = ((moe_d_ff + 255) // 256) * 256
|
| 176 |
+
for name, n_el in ne_map.items():
|
| 177 |
+
ttype = importance_table.get(name, {}).get("type", get_tensor_type(name))
|
| 178 |
+
if ttype in MOE_PAD_TYPES:
|
| 179 |
+
padded_ne_map[name] = (n_el // moe_d_ff) * aligned_d_ff
|
| 180 |
+
# -----------------------------------------------------------
|
| 181 |
+
|
| 182 |
+
all_names = set(ne_map.keys())
|
| 183 |
+
mtp_names = {n for n in all_names if is_mtp_tensor(n, n_layers)} if has_mtp else set()
|
| 184 |
+
non_mtp_names = all_names - mtp_names
|
| 185 |
+
|
| 186 |
+
tensor_importance = {}
|
| 187 |
+
for name in non_mtp_names:
|
| 188 |
+
raw_imp = importance_table.get(name, {}).get("importance_mean", 0.0)
|
| 189 |
+
tensor_importance[name] = raw_imp
|
| 190 |
+
|
| 191 |
+
assignments = compute_initial_assignments(non_mtp_names, mtp_names, importance_table, allow_q3, is_qat)
|
| 192 |
+
group_registry = build_groups(tied_groups, non_mtp_names, ne_map, padded_ne_map)
|
| 193 |
+
|
| 194 |
+
mtp_cost = sum(_size_mib(MTP_DEPLOY_TIER, ne_map.get(n, 0)) for n in mtp_names)
|
| 195 |
+
effective_target = target_size_mib - mtp_cost
|
| 196 |
+
|
| 197 |
+
current_size = sum(
|
| 198 |
+
_size_mib(assignments[n], padded_ne_map.get(n, ne_map.get(n, 0)) if assignments[n] in K_QUANTS else ne_map.get(n, 0))
|
| 199 |
+
for n in non_mtp_names
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
if current_size > effective_target:
|
| 203 |
+
warnings.warn(f"Initial size {current_size:.1f} MiB already exceeds target {effective_target:.1f} MiB", RuntimeWarning)
|
| 204 |
+
|
| 205 |
+
upgrade_queue = []
|
| 206 |
+
for g_id in group_registry:
|
| 207 |
+
_push_upgrade(g_id, group_registry, assignments, tensor_importance, upgrade_queue, importance_table)
|
| 208 |
+
|
| 209 |
+
while upgrade_queue:
|
| 210 |
+
item = heapq.heappop(upgrade_queue)
|
| 211 |
+
g_id, next_tier, cost_delta = item.group_id, item.next_tier, item.cost_delta
|
| 212 |
+
|
| 213 |
+
if cost_delta > 0 and current_size + cost_delta > effective_target:
|
| 214 |
+
continue
|
| 215 |
+
|
| 216 |
+
for n in group_registry[g_id][0]:
|
| 217 |
+
assignments[n] = next_tier
|
| 218 |
+
current_size += cost_delta
|
| 219 |
+
|
| 220 |
+
_push_upgrade(g_id, group_registry, assignments, tensor_importance, upgrade_queue, importance_table)
|
| 221 |
+
|
| 222 |
+
return assignments, padded_ne_map
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def compute_stats(assignments: dict, ne_map: dict = None, padded_ne_map: dict = None) -> dict:
|
| 226 |
+
"""Собирает статистику по тирам с точным учётом K_QUANTS padding."""
|
| 227 |
+
stats = {"by_tier_count": {}, "by_tier_mib": {}, "total_mib": 0.0, "tensor_count": 0}
|
| 228 |
+
for name, tier in assignments.items():
|
| 229 |
+
if not isinstance(tier, str):
|
| 230 |
+
continue
|
| 231 |
+
stats["tensor_count"] += 1
|
| 232 |
+
stats["by_tier_count"][tier] = stats["by_tier_count"].get(tier, 0) + 1
|
| 233 |
+
|
| 234 |
+
if ne_map:
|
| 235 |
+
if padded_ne_map and tier in K_QUANTS:
|
| 236 |
+
elements = padded_ne_map.get(name, ne_map.get(name, 0))
|
| 237 |
+
else:
|
| 238 |
+
elements = ne_map.get(name, 0)
|
| 239 |
+
|
| 240 |
+
size = _size_mib(tier, elements)
|
| 241 |
+
stats["by_tier_mib"][tier] = stats["by_tier_mib"].get(tier, 0.0) + size
|
| 242 |
+
stats["total_mib"] += size
|
| 243 |
+
|
| 244 |
+
return stats
|
config_generator.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from constants import QUANT_RANK
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def get_regex_priority(regex: str) -> int:
|
| 6 |
+
"""Higher = more specific = should come first (first-match-wins)."""
|
| 7 |
+
score = 0
|
| 8 |
+
|
| 9 |
+
if "nextn" in regex:
|
| 10 |
+
score += 200
|
| 11 |
+
if re.search(r"(blk|BLK)\\.3[0-2]\\.", regex):
|
| 12 |
+
score += 100
|
| 13 |
+
if re.search(r"(blk|BLK)\\.0\\.", regex):
|
| 14 |
+
score += 90
|
| 15 |
+
if re.search(r"(blk|BLK)\\.31\\.", regex):
|
| 16 |
+
score += 80
|
| 17 |
+
# Layer-range group: (blk|BLK)\.( — more specific than \d+
|
| 18 |
+
if r"(blk|BLK)\.(" in regex:
|
| 19 |
+
score += 50
|
| 20 |
+
elif r"(blk|BLK)\.\d" in regex:
|
| 21 |
+
score += 30
|
| 22 |
+
if regex.startswith(".*"):
|
| 23 |
+
score -= 50
|
| 24 |
+
if regex.endswith(r"\.weight"):
|
| 25 |
+
score += 10
|
| 26 |
+
|
| 27 |
+
return score
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _is_contiguous(lst, low, high):
|
| 31 |
+
if not lst:
|
| 32 |
+
return False
|
| 33 |
+
return len(lst) == (high - low + 1)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _group_ranges(lst):
|
| 37 |
+
if not lst:
|
| 38 |
+
return
|
| 39 |
+
start = lst[0]
|
| 40 |
+
end = lst[0]
|
| 41 |
+
for i in range(1, len(lst)):
|
| 42 |
+
if lst[i] == end + 1:
|
| 43 |
+
end = lst[i]
|
| 44 |
+
else:
|
| 45 |
+
yield (start, end)
|
| 46 |
+
start = end = lst[i]
|
| 47 |
+
yield (start, end)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _range_to_regex(start: int, end: int) -> str:
|
| 51 |
+
"""Convert a range of layer numbers [start, end] to a valid regex."""
|
| 52 |
+
if start == end:
|
| 53 |
+
return str(start)
|
| 54 |
+
# For single-digit ranges, use character class
|
| 55 |
+
if end <= 9:
|
| 56 |
+
return f"[{start}-{end}]"
|
| 57 |
+
# Enumerate all numbers as pipe alternatives
|
| 58 |
+
alt = "|".join(str(i) for i in range(start, end + 1))
|
| 59 |
+
return f"(?:{alt})"
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def generate_flags(
|
| 63 |
+
assignments: dict,
|
| 64 |
+
model: dict,
|
| 65 |
+
base_type: str,
|
| 66 |
+
target_size_mib: float = None,
|
| 67 |
+
) -> dict:
|
| 68 |
+
is_qat = model.get("features", {}).get("is_qat", False)
|
| 69 |
+
output_type = "Q5_K"
|
| 70 |
+
token_embd_type = "Q4_K" if is_qat else "Q5_K"
|
| 71 |
+
|
| 72 |
+
max_layer = model.get("features", {}).get("n_layers", 31)
|
| 73 |
+
|
| 74 |
+
rules = []
|
| 75 |
+
|
| 76 |
+
# Group blk tensors by (ttype, tier)
|
| 77 |
+
type_tier_layers = {}
|
| 78 |
+
for tname, tier in assignments.items():
|
| 79 |
+
parts = tname.split(".")
|
| 80 |
+
if len(parts) >= 3 and parts[0] in ("blk", "BLK"):
|
| 81 |
+
try:
|
| 82 |
+
layer = int(parts[1])
|
| 83 |
+
except ValueError:
|
| 84 |
+
continue
|
| 85 |
+
ttype = parts[2]
|
| 86 |
+
key = (ttype, tier)
|
| 87 |
+
if key not in type_tier_layers:
|
| 88 |
+
type_tier_layers[key] = []
|
| 89 |
+
type_tier_layers[key].append(layer)
|
| 90 |
+
|
| 91 |
+
# Generate rules for blk tensor groups
|
| 92 |
+
for (ttype, tier), layers in sorted(
|
| 93 |
+
type_tier_layers.items(),
|
| 94 |
+
key=lambda x: -QUANT_RANK.get(x[0][1], 0),
|
| 95 |
+
):
|
| 96 |
+
layers = sorted(set(layers))
|
| 97 |
+
|
| 98 |
+
if len(layers) >= 8 and _is_contiguous(layers, 0, max_layer):
|
| 99 |
+
pattern = f"(blk|BLK)\\.\\d+\\.{ttype}={tier}"
|
| 100 |
+
else:
|
| 101 |
+
parts = []
|
| 102 |
+
for start, end in _group_ranges(layers):
|
| 103 |
+
if start == end:
|
| 104 |
+
parts.append(str(start))
|
| 105 |
+
else:
|
| 106 |
+
parts.append(_range_to_regex(start, end))
|
| 107 |
+
desc = "|".join(parts)
|
| 108 |
+
pattern = f"(blk|BLK)\\.({desc})\\.{ttype}={tier}"
|
| 109 |
+
|
| 110 |
+
prio = get_regex_priority(pattern) + (
|
| 111 |
+
10 if tier == "Q8_0" else 5 if tier == "Q6_K" else 0
|
| 112 |
+
) + (5 if len(layers) == 1 else 0) + (3 if "ffn_down" in ttype else 0)
|
| 113 |
+
|
| 114 |
+
rules.append((pattern, prio))
|
| 115 |
+
|
| 116 |
+
# Generate rules for global tensors (non-blk)
|
| 117 |
+
prefix = model.get("features", {}).get("prefix", "blk")
|
| 118 |
+
for tname, tier in assignments.items():
|
| 119 |
+
parts = tname.split(".")
|
| 120 |
+
if len(parts) >= 2 and parts[0].lower() == prefix.lower():
|
| 121 |
+
continue
|
| 122 |
+
ttype = parts[0] if len(parts) >= 1 else tname
|
| 123 |
+
if ttype in ("token_embd", "output"):
|
| 124 |
+
continue
|
| 125 |
+
if ttype == tname and "." in tname:
|
| 126 |
+
# e.g. "nextn.eh_proj" without blk prefix
|
| 127 |
+
pass
|
| 128 |
+
# Check that this global tensor wasn't already handled as a blk tensor
|
| 129 |
+
pattern = f".*{re.escape(ttype)}.*={tier}"
|
| 130 |
+
prio = get_regex_priority(pattern) + (5 if tier == "Q8_0" else 0)
|
| 131 |
+
# Deduplicate (same pattern may appear from different names)
|
| 132 |
+
if not any(p == pattern for p, _ in rules):
|
| 133 |
+
rules.append((pattern, prio))
|
| 134 |
+
|
| 135 |
+
rules.sort(key=lambda x: -x[1])
|
| 136 |
+
|
| 137 |
+
flags = {
|
| 138 |
+
"imatrix": None,
|
| 139 |
+
"output_tensor_type": output_type,
|
| 140 |
+
"token_embedding_type": token_embd_type,
|
| 141 |
+
"tensor_type_rules": [f'--tensor-type "{r[0]}"' for r in rules],
|
| 142 |
+
"base_type": base_type,
|
| 143 |
+
"target_size_mib": target_size_mib,
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
return flags
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def format_flags(flags: dict) -> str:
|
| 150 |
+
lines = []
|
| 151 |
+
lines.append(" --output-tensor-type " + flags["output_tensor_type"])
|
| 152 |
+
lines.append(" --token-embedding-type " + flags["token_embedding_type"])
|
| 153 |
+
for rule in flags["tensor_type_rules"]:
|
| 154 |
+
lines.append(" " + rule)
|
| 155 |
+
return "\n".join(lines)
|
constants.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
GGUF_TYPE_NAMES = {
|
| 4 |
+
0: "F32",
|
| 5 |
+
1: "F16",
|
| 6 |
+
2: "Q4_0", 3: "Q4_1", 6: "Q5_0", 7: "Q5_1",
|
| 7 |
+
8: "Q8_0",
|
| 8 |
+
10: "Q4_K", 11: "Q5_K", 12: "Q6_K",
|
| 9 |
+
13: "Q5_K_M", 14: "Q4_K_M",
|
| 10 |
+
15: "IQ4_XS", 16: "IQ4_NL",
|
| 11 |
+
20: "IQ3_XXS",
|
| 12 |
+
24: "IQ2_XXS",
|
| 13 |
+
30: "IQ1_S",
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
GGUF_TYPE_NAMES_INV = {v: k for k, v in GGUF_TYPE_NAMES.items()}
|
| 17 |
+
|
| 18 |
+
# Ordered from worst to best quality
|
| 19 |
+
TIER_ORDER = [
|
| 20 |
+
"IQ1_S", "IQ2_XXS", "IQ2_XS", "IQ2_S",
|
| 21 |
+
"IQ3_XXS", "Q3_K", "IQ3_S",
|
| 22 |
+
"IQ4_XS", "IQ4_NL", "Q4_K", "Q5_K", "Q6_K", "Q8_0", "F16",
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
# Exact bits per weight from ggml block structs (ggml_type_sizef * 8)
|
| 26 |
+
# Does NOT include GGUF overhead — GGUF_OVERHEAD_FACTOR is applied separately
|
| 27 |
+
TIER_BPW = {
|
| 28 |
+
"IQ1_S": 1.5625,
|
| 29 |
+
"IQ2_XXS": 2.0625,
|
| 30 |
+
"IQ2_XS": 2.3125,
|
| 31 |
+
"IQ2_S": 2.5,
|
| 32 |
+
"IQ3_XXS": 3.0625,
|
| 33 |
+
"Q3_K": 3.4375,
|
| 34 |
+
"IQ3_S": 3.44,
|
| 35 |
+
"IQ4_XS": 4.25,
|
| 36 |
+
"IQ4_NL": 4.5,
|
| 37 |
+
"Q4_K": 4.5,
|
| 38 |
+
"Q5_K": 5.5,
|
| 39 |
+
"Q6_K": 6.5625,
|
| 40 |
+
"Q8_0": 8.5,
|
| 41 |
+
"F16": 16.0,
|
| 42 |
+
}
|
| 43 |
+
GGUF_OVERHEAD_FACTOR = 1.0
|
| 44 |
+
|
| 45 |
+
# Quality retention per tier (1.0 = F16, no loss). Used for non-linear utility.
|
| 46 |
+
QUALITY_WEIGHTS = {
|
| 47 |
+
"F16": 1.000,
|
| 48 |
+
"Q8_0": 0.995,
|
| 49 |
+
"Q6_K": 0.990,
|
| 50 |
+
"Q5_K": 0.978,
|
| 51 |
+
"IQ4_XS": 0.940,
|
| 52 |
+
"IQ4_NL": 0.945,
|
| 53 |
+
"Q4_K": 0.960,
|
| 54 |
+
"Q3_K": 0.900,
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
# Quant quality rank: higher = better (same order as TIER_ORDER)
|
| 58 |
+
QUANT_RANK = {tier: i for i, tier in enumerate(TIER_ORDER)}
|
| 59 |
+
|
| 60 |
+
# Per-class hard floor — NEVER go below this without --allow-q3-or-lower
|
| 61 |
+
# Matches hand-tuned v2 patterns: gate=Q6_K, attn_proj=Q6_K, ffn=IQ4_XS
|
| 62 |
+
CLASS_HARD_FLOORS = {
|
| 63 |
+
"gate": "Q8_0",
|
| 64 |
+
"attn_proj": "Q8_0",
|
| 65 |
+
"ffn_gate_up": "IQ4_XS",
|
| 66 |
+
"ffn_down": "Q6_K",
|
| 67 |
+
"norms": "F16",
|
| 68 |
+
"ssm_params": "F16",
|
| 69 |
+
"mtp": "IQ4_XS",
|
| 70 |
+
"embd": "Q5_K",
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
# Per-class start tier — where greedy begins (Q5_K for all, like OptA base type)
|
| 74 |
+
# Greedy upgrades from here toward CLASS_MAX_TIER
|
| 75 |
+
CLASS_START_TIER = {
|
| 76 |
+
"gate": "Q8_0",
|
| 77 |
+
"attn_proj": "Q8_0",
|
| 78 |
+
"ffn_gate_up": "Q5_K",
|
| 79 |
+
"ffn_down": "Q6_K",
|
| 80 |
+
"norms": "F16",
|
| 81 |
+
"ssm_params": "F16",
|
| 82 |
+
"mtp": "Q5_K",
|
| 83 |
+
"embd": "Q5_K",
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
# Per-class max tier — never exceed this (for greedy upgrades)
|
| 87 |
+
# Deep layers can be upgraded to Q8_0 for important tensors
|
| 88 |
+
CLASS_MAX_TIER = {
|
| 89 |
+
"gate": "Q8_0",
|
| 90 |
+
"attn_proj": "Q8_0",
|
| 91 |
+
"ffn_gate_up": "Q8_0",
|
| 92 |
+
"ffn_down": "Q8_0",
|
| 93 |
+
"norms": "F16",
|
| 94 |
+
"ssm_params": "F16",
|
| 95 |
+
"mtp": "Q8_0",
|
| 96 |
+
"embd": "Q5_K",
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
# Classes that can go to Q3_K when --allow-q3-or-lower is set
|
| 100 |
+
CAN_Q3 = {"ffn_gate", "ffn_up", "ffn_down", "attn_output", "ssm_out"}
|
| 101 |
+
ALLOW_LOWER_FLOOR = "IQ2_XXS" # lowest starting tier for --allow-q3-or-lower
|
| 102 |
+
|
| 103 |
+
# Default floor for unclassified tensors / unknown class
|
| 104 |
+
DEFAULT_FLOOR = "Q4_K"
|
| 105 |
+
|
| 106 |
+
# Tier for MTP head deployment
|
| 107 |
+
MTP_DEPLOY_TIER = "Q8_0"
|
| 108 |
+
|
| 109 |
+
# Preferred tier for extreme importance spikes (>50% of total importance)
|
| 110 |
+
SPIKE_IMPORTANCE_RATIO = 0.50
|
| 111 |
+
SPIKE_PREFERRED_TIER = "F16"
|
| 112 |
+
|
| 113 |
+
TENSOR_CLASS = {
|
| 114 |
+
# Qwen 3.5 hybrid
|
| 115 |
+
"attn_gate": "gate",
|
| 116 |
+
"ssm_alpha": "gate",
|
| 117 |
+
"ssm_beta": "gate",
|
| 118 |
+
"attn_q": "attn_proj",
|
| 119 |
+
"attn_k": "attn_proj",
|
| 120 |
+
"attn_v": "attn_proj",
|
| 121 |
+
"attn_qkv": "attn_proj",
|
| 122 |
+
"attn_output": "attn_proj",
|
| 123 |
+
"ffn_gate": "ffn_gate_up",
|
| 124 |
+
"ffn_up": "ffn_gate_up",
|
| 125 |
+
"ffn_down": "ffn_down",
|
| 126 |
+
"ssm_out": "ffn_down",
|
| 127 |
+
"ssm_conv1d": "norms",
|
| 128 |
+
"router": "norms",
|
| 129 |
+
"ssm_dt": "ssm_params",
|
| 130 |
+
"ssm_a": "ssm_params",
|
| 131 |
+
"nextn": "mtp",
|
| 132 |
+
"ffn_gate_exps": "ffn_gate_up",
|
| 133 |
+
"ffn_up_exps": "ffn_gate_up",
|
| 134 |
+
"ffn_down_exps": "ffn_down",
|
| 135 |
+
"ffn_gate_inp": "norms",
|
| 136 |
+
|
| 137 |
+
# Standard llama.cpp tensor names
|
| 138 |
+
"q_proj": "attn_proj",
|
| 139 |
+
"k_proj": "attn_proj",
|
| 140 |
+
"v_proj": "attn_proj",
|
| 141 |
+
"o_proj": "attn_proj",
|
| 142 |
+
"gate_proj": "ffn_gate_up",
|
| 143 |
+
"up_proj": "ffn_gate_up",
|
| 144 |
+
"down_proj": "ffn_down",
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
ARCH_FEATURES = {
|
| 148 |
+
"qwen35": {
|
| 149 |
+
"has_qkv": True,
|
| 150 |
+
"has_ssm": True,
|
| 151 |
+
"has_mtp": True,
|
| 152 |
+
"has_moe": False,
|
| 153 |
+
"is_qat": False,
|
| 154 |
+
"prefix": "blk",
|
| 155 |
+
"n_layers": 32,
|
| 156 |
+
},
|
| 157 |
+
"mellum2": {
|
| 158 |
+
"has_qkv": False,
|
| 159 |
+
"has_ssm": False,
|
| 160 |
+
"has_mtp": False,
|
| 161 |
+
"has_moe": True,
|
| 162 |
+
"is_qat": False,
|
| 163 |
+
"prefix": "blk",
|
| 164 |
+
"n_layers": 28,
|
| 165 |
+
},
|
| 166 |
+
"gemma4": {
|
| 167 |
+
"has_qkv": False,
|
| 168 |
+
"has_ssm": False,
|
| 169 |
+
"has_mtp": False,
|
| 170 |
+
"has_moe": False,
|
| 171 |
+
"is_qat": True,
|
| 172 |
+
"prefix": "blk",
|
| 173 |
+
"n_layers": 48,
|
| 174 |
+
},
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def strip_weight(name: str) -> str:
|
| 179 |
+
return name.lstrip(".").removesuffix(".weight").removesuffix(".bias")
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def get_tensor_type(name: str) -> str:
|
| 183 |
+
parts = strip_weight(name).split(".")
|
| 184 |
+
if len(parts) >= 2 and parts[0] in ("blk", "BLK"):
|
| 185 |
+
return parts[2] if len(parts) >= 3 else "unknown"
|
| 186 |
+
if "token_embd" in name:
|
| 187 |
+
return "token_embd"
|
| 188 |
+
if name.startswith("output") and "norm" not in name:
|
| 189 |
+
return "output"
|
| 190 |
+
return name
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def get_tensor_class(ttype: str) -> str:
|
| 194 |
+
if ttype in TENSOR_CLASS:
|
| 195 |
+
return TENSOR_CLASS[ttype]
|
| 196 |
+
if "norm" in ttype or "scale" in ttype:
|
| 197 |
+
return "norms"
|
| 198 |
+
if ttype.startswith("ssm_"):
|
| 199 |
+
return "ssm_params"
|
| 200 |
+
if ttype in ("token_embd", "output", "embed_tokens", "lm_head",
|
| 201 |
+
"vision_embedder", "audio_embedder"):
|
| 202 |
+
return "embd"
|
| 203 |
+
return "unknown"
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def is_mtp_tensor(name: str, n_layers: int = 32) -> bool:
|
| 207 |
+
if "nextn" in name:
|
| 208 |
+
return True
|
| 209 |
+
if n_layers > 40:
|
| 210 |
+
return False # Deep models (Gemma4, 48 layers) use separate drafter, not in-model MTP
|
| 211 |
+
layer = get_layer_number(name)
|
| 212 |
+
return layer is not None and layer >= n_layers
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def get_layer_number(name: str) -> int | None:
|
| 216 |
+
parts = strip_weight(name).split(".")
|
| 217 |
+
if len(parts) >= 2 and parts[0] in ("blk", "BLK"):
|
| 218 |
+
try:
|
| 219 |
+
return int(parts[1])
|
| 220 |
+
except ValueError:
|
| 221 |
+
return None
|
| 222 |
+
return None
|
imatrix_reader.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gguf
|
| 2 |
+
import numpy as np
|
| 3 |
+
from typing import List
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def read_imatrix(path: str) -> dict:
|
| 7 |
+
"""Parse imatrix GGUF, return per-tensor importance data."""
|
| 8 |
+
r = gguf.GGUFReader(path)
|
| 9 |
+
raw = {}
|
| 10 |
+
meta = {}
|
| 11 |
+
|
| 12 |
+
for k, v in r.fields.items():
|
| 13 |
+
try:
|
| 14 |
+
meta[k] = v.data
|
| 15 |
+
except:
|
| 16 |
+
meta[k] = str(v)
|
| 17 |
+
|
| 18 |
+
for t in r.tensors:
|
| 19 |
+
name = t.name
|
| 20 |
+
arr = np.array(t.data, dtype=np.float64)
|
| 21 |
+
|
| 22 |
+
if name.endswith(".in_sum2"):
|
| 23 |
+
base = name[:-8]
|
| 24 |
+
if base not in raw:
|
| 25 |
+
raw[base] = {}
|
| 26 |
+
raw[base]["in_sum2"] = arr
|
| 27 |
+
elif name.endswith(".counts"):
|
| 28 |
+
base = name[:-7]
|
| 29 |
+
if base not in raw:
|
| 30 |
+
raw[base] = {}
|
| 31 |
+
raw[base]["counts"] = float(np.mean(arr))
|
| 32 |
+
|
| 33 |
+
result = {}
|
| 34 |
+
for base, data in raw.items():
|
| 35 |
+
if "in_sum2" not in data:
|
| 36 |
+
continue
|
| 37 |
+
arr = data["in_sum2"]
|
| 38 |
+
result[base] = {
|
| 39 |
+
"importance_mean": float(np.mean(arr)),
|
| 40 |
+
"importance_sum": float(np.sum(arr)),
|
| 41 |
+
"importance_max": float(np.max(arr)),
|
| 42 |
+
"importance_min": float(np.min(arr)),
|
| 43 |
+
"n_elements": arr.size,
|
| 44 |
+
"in_sum2_raw": arr,
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
return {
|
| 48 |
+
"path": path,
|
| 49 |
+
"tensors": result,
|
| 50 |
+
"n_tensors": len(result),
|
| 51 |
+
"meta": meta,
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def combine_imatrix(imatrix_list: List[dict], method: str = "max") -> dict:
|
| 56 |
+
"""Combine multiple imatrix into one by aggregating importance.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
imatrix_list: List of imatrix dicts from read_imatrix()
|
| 60 |
+
method: "max", "mean", or "weighted_mean"
|
| 61 |
+
"""
|
| 62 |
+
if not imatrix_list:
|
| 63 |
+
return {}
|
| 64 |
+
if len(imatrix_list) == 1:
|
| 65 |
+
return imatrix_list[0]
|
| 66 |
+
|
| 67 |
+
# Get all tensor names across all imatrix
|
| 68 |
+
all_names = set()
|
| 69 |
+
for im in imatrix_list:
|
| 70 |
+
all_names.update(im["tensors"].keys())
|
| 71 |
+
|
| 72 |
+
combined_tensors = {}
|
| 73 |
+
for name in all_names:
|
| 74 |
+
vals = []
|
| 75 |
+
in_sum2_raw = None
|
| 76 |
+
n_elements = 0
|
| 77 |
+
|
| 78 |
+
for im in imatrix_list:
|
| 79 |
+
if name in im["tensors"]:
|
| 80 |
+
t = im["tensors"][name]
|
| 81 |
+
vals.append(t["importance_mean"])
|
| 82 |
+
if in_sum2_raw is None and "in_sum2_raw" in t:
|
| 83 |
+
in_sum2_raw = t["in_sum2_raw"]
|
| 84 |
+
n_elements = max(n_elements, t["n_elements"])
|
| 85 |
+
|
| 86 |
+
if not vals:
|
| 87 |
+
continue
|
| 88 |
+
|
| 89 |
+
if method == "max":
|
| 90 |
+
imp_mean = max(vals)
|
| 91 |
+
elif method == "mean":
|
| 92 |
+
imp_mean = sum(vals) / len(vals)
|
| 93 |
+
elif method == "weighted_mean":
|
| 94 |
+
# Could weight by n_elements or dataset size
|
| 95 |
+
imp_mean = sum(vals) / len(vals)
|
| 96 |
+
else:
|
| 97 |
+
imp_mean = max(vals)
|
| 98 |
+
|
| 99 |
+
combined_tensors[name] = {
|
| 100 |
+
"importance_mean": imp_mean,
|
| 101 |
+
"importance_sum": imp_mean * n_elements,
|
| 102 |
+
"importance_max": max(v.get("importance_max", 0) for im in imatrix_list if name in im["tensors"] for v in [im["tensors"][name]]),
|
| 103 |
+
"importance_min": min(v.get("importance_min", float('inf')) for im in imatrix_list if name in im["tensors"] for v in [im["tensors"][name]]),
|
| 104 |
+
"n_elements": n_elements,
|
| 105 |
+
"in_sum2_raw": in_sum2_raw,
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
# Merge metadata
|
| 109 |
+
combined_meta = {}
|
| 110 |
+
for im in imatrix_list:
|
| 111 |
+
for k, v in im["meta"].items():
|
| 112 |
+
if k not in combined_meta:
|
| 113 |
+
combined_meta[k] = v
|
| 114 |
+
|
| 115 |
+
return {
|
| 116 |
+
"path": "+".join(im["path"] for im in imatrix_list),
|
| 117 |
+
"tensors": combined_tensors,
|
| 118 |
+
"n_tensors": len(combined_tensors),
|
| 119 |
+
"meta": combined_meta,
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def detect_tied_groups(imatrix: dict, atol: float = 1e-5) -> list:
|
| 124 |
+
"""Find tied tensor groups (identical importance arrays)."""
|
| 125 |
+
names = sorted(imatrix["tensors"].keys())
|
| 126 |
+
tied_groups = []
|
| 127 |
+
visited = set()
|
| 128 |
+
|
| 129 |
+
for i, n1 in enumerate(names):
|
| 130 |
+
if n1 in visited:
|
| 131 |
+
continue
|
| 132 |
+
group = [n1]
|
| 133 |
+
arr1 = imatrix["tensors"][n1].get("in_sum2_raw")
|
| 134 |
+
if arr1 is None:
|
| 135 |
+
tied_groups.append(group)
|
| 136 |
+
visited.add(n1)
|
| 137 |
+
continue
|
| 138 |
+
for j in range(i + 1, len(names)):
|
| 139 |
+
n2 = names[j]
|
| 140 |
+
if n2 in visited:
|
| 141 |
+
continue
|
| 142 |
+
arr2 = imatrix["tensors"][n2].get("in_sum2_raw")
|
| 143 |
+
if arr2 is None:
|
| 144 |
+
continue
|
| 145 |
+
if arr1.shape == arr2.shape and np.allclose(arr1, arr2, atol=atol):
|
| 146 |
+
group.append(n2)
|
| 147 |
+
visited.add(n2)
|
| 148 |
+
tied_groups.append(group)
|
| 149 |
+
visited.add(n1)
|
| 150 |
+
|
| 151 |
+
return tied_groups
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def build_importance_table(imatrix: dict, model: dict) -> dict:
|
| 155 |
+
"""Build unified importance table, merging imatrix with model tensor info."""
|
| 156 |
+
table = {}
|
| 157 |
+
for tname, info in imatrix["tensors"].items():
|
| 158 |
+
ttype = _imatrix_type(tname)
|
| 159 |
+
table[tname] = {
|
| 160 |
+
"importance_mean": info["importance_mean"],
|
| 161 |
+
"importance_sum": info["importance_sum"],
|
| 162 |
+
"importance_max": info["importance_max"],
|
| 163 |
+
"importance_min": info["importance_min"],
|
| 164 |
+
"n_elements": info["n_elements"],
|
| 165 |
+
"type": ttype,
|
| 166 |
+
}
|
| 167 |
+
# Also index by name without trailing dot (safety for old code)
|
| 168 |
+
for tname, info in list(table.items()):
|
| 169 |
+
if tname.endswith("."):
|
| 170 |
+
alt = tname.rstrip(".")
|
| 171 |
+
table[alt] = info
|
| 172 |
+
return table
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def _imatrix_type(name: str) -> str:
|
| 176 |
+
parts = name.split(".")
|
| 177 |
+
if len(parts) >= 3 and parts[0] == "blk":
|
| 178 |
+
return parts[2]
|
| 179 |
+
return name
|
main.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
import sys
|
| 7 |
+
|
| 8 |
+
from model_reader import read_model
|
| 9 |
+
from imatrix_reader import read_imatrix, detect_tied_groups, build_importance_table
|
| 10 |
+
from classifier import optimal_classify, compute_stats
|
| 11 |
+
from config_generator import generate_flags, format_flags
|
| 12 |
+
from quantizer import run_dry_run, run_quantization
|
| 13 |
+
from constants import CLASS_HARD_FLOORS
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _get_base_type(model: dict) -> str:
|
| 17 |
+
is_qat = model.get("features", {}).get("is_qat", False)
|
| 18 |
+
return "IQ4_XS" if is_qat else "Q5_K_M"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def main():
|
| 22 |
+
parser = argparse.ArgumentParser(
|
| 23 |
+
description="SHQ-program: imatrix-driven hybrid quantization"
|
| 24 |
+
)
|
| 25 |
+
parser.add_argument("--model", help="BF16 GGUF model path")
|
| 26 |
+
parser.add_argument("--imatrix", action="append", default=[],
|
| 27 |
+
help="Imatrix GGUF path (can be specified multiple times)")
|
| 28 |
+
parser.add_argument("--imatrix-method", choices=["max", "mean"], default="max",
|
| 29 |
+
help="How to combine multiple imatrix: max (conservative) or mean (default: max)")
|
| 30 |
+
parser.add_argument("--size", type=float, default=6800,
|
| 31 |
+
help="Target file size in MiB (default: 6800 = ~6.6 GB)")
|
| 32 |
+
parser.add_argument("--output", default=None, help="Output GGUF path")
|
| 33 |
+
parser.add_argument("--run", action="store_true", help="Execute quantization")
|
| 34 |
+
parser.add_argument("--show-config", action="store_true", help="Print config and exit")
|
| 35 |
+
parser.add_argument("--verbose", action="store_true", help="Detailed output")
|
| 36 |
+
parser.add_argument("--allow-q3-or-lower", action="store_true",
|
| 37 |
+
help="Allow Q3_K for low-importance tensors (risk of quality loss)")
|
| 38 |
+
parser.add_argument("--aggro", type=float, default=None,
|
| 39 |
+
help="[deprecated] Use --size instead")
|
| 40 |
+
parser.add_argument("--show-floors", action="store_true",
|
| 41 |
+
help="Print class hard floors and exit")
|
| 42 |
+
|
| 43 |
+
args = parser.parse_args()
|
| 44 |
+
|
| 45 |
+
if args.show_floors:
|
| 46 |
+
_show_floors()
|
| 47 |
+
return
|
| 48 |
+
|
| 49 |
+
if not args.model or not args.imatrix:
|
| 50 |
+
parser.print_usage()
|
| 51 |
+
print("main.py: error: --model and --imatrix are required")
|
| 52 |
+
sys.exit(1)
|
| 53 |
+
|
| 54 |
+
target_mib = args.size
|
| 55 |
+
|
| 56 |
+
print("=== SHQ-program ===")
|
| 57 |
+
print(f"Model: {args.model}")
|
| 58 |
+
if len(args.imatrix) == 1:
|
| 59 |
+
print(f"Imatrix: {args.imatrix[0]}")
|
| 60 |
+
else:
|
| 61 |
+
print(f"Imatrix: {len(args.imatrix)} files ({args.imatrix_method})")
|
| 62 |
+
for p in args.imatrix:
|
| 63 |
+
print(f" - {p}")
|
| 64 |
+
print(f"Target: {target_mib:.0f} MiB ({target_mib / 1024:.2f} GB)")
|
| 65 |
+
if args.allow_q3_or_lower:
|
| 66 |
+
print(" --allow-q3-or-lower: low-importance tensors may go to Q3_K")
|
| 67 |
+
print()
|
| 68 |
+
|
| 69 |
+
print("[1/4] Reading model...")
|
| 70 |
+
model = read_model(args.model)
|
| 71 |
+
print(f" Architecture: {model['architecture']}")
|
| 72 |
+
print(f" Tensors: {model['n_tensors']}")
|
| 73 |
+
print(f" Features: {json.dumps(model['features'], indent=2)}")
|
| 74 |
+
|
| 75 |
+
print("\n[2/4] Reading imatrix...")
|
| 76 |
+
imatrix_list = [read_imatrix(p) for p in args.imatrix]
|
| 77 |
+
for im in imatrix_list:
|
| 78 |
+
print(f" {im['path']}: {im['n_tensors']} tensors, datasets={im['meta'].get('imatrix.datasets', '?')}")
|
| 79 |
+
|
| 80 |
+
from imatrix_reader import combine_imatrix
|
| 81 |
+
imatrix = combine_imatrix(imatrix_list, method=args.imatrix_method)
|
| 82 |
+
print(f" Combined: {imatrix['n_tensors']} tensors")
|
| 83 |
+
|
| 84 |
+
print("\n[3/4] Detecting tied groups...")
|
| 85 |
+
tied_groups = detect_tied_groups(imatrix)
|
| 86 |
+
print(f" Found {len(tied_groups)} tied groups:")
|
| 87 |
+
for g in tied_groups:
|
| 88 |
+
if len(g) > 1:
|
| 89 |
+
print(f" TIED ({len(g)}): {g[0].replace('.weight', '')} = "
|
| 90 |
+
f"{g[1].replace('.weight', '')}")
|
| 91 |
+
|
| 92 |
+
imp_table = build_importance_table(imatrix, model)
|
| 93 |
+
|
| 94 |
+
print("\n[4/4] Classifying tensors (greedy imatrix-driven)...")
|
| 95 |
+
|
| 96 |
+
# Получаем и маппинг тиров, и точную карту паддингов напрямую из классификатора
|
| 97 |
+
assignments, padded_ne_map = optimal_classify(
|
| 98 |
+
imp_table, tied_groups, model,
|
| 99 |
+
target_size_mib=target_mib,
|
| 100 |
+
allow_q3=args.allow_q3_or_lower,
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
ne_map = {k: v["n_elements"] for k, v in model.get("tensors", {}).items()}
|
| 104 |
+
for tname, info in imp_table.items():
|
| 105 |
+
if tname not in ne_map:
|
| 106 |
+
ne_map[tname] = info["n_elements"]
|
| 107 |
+
|
| 108 |
+
# Передаем padded_ne_map для корректного вывода логов на экран
|
| 109 |
+
_show_tier_summary(assignments, imp_table, ne_map, padded_ne_map)
|
| 110 |
+
|
| 111 |
+
base_type = _get_base_type(model)
|
| 112 |
+
flags = generate_flags(assignments, model, base_type, target_mib)
|
| 113 |
+
flags["imatrix"] = args.imatrix
|
| 114 |
+
|
| 115 |
+
print(f"\nConfig (base={flags['base_type']}):")
|
| 116 |
+
print(format_flags(flags))
|
| 117 |
+
|
| 118 |
+
if args.show_config:
|
| 119 |
+
return
|
| 120 |
+
|
| 121 |
+
print("\n--- Dry Run ---")
|
| 122 |
+
dry_size = run_dry_run(flags, args.model)
|
| 123 |
+
_show_size_result(dry_size, target_mib)
|
| 124 |
+
|
| 125 |
+
if not args.run:
|
| 126 |
+
print("\nDry run only. Use --run to execute quantization.")
|
| 127 |
+
return
|
| 128 |
+
|
| 129 |
+
if not args.output:
|
| 130 |
+
base = os.path.splitext(os.path.basename(args.model))[0]
|
| 131 |
+
args.output = base + "-SHQ.gguf"
|
| 132 |
+
|
| 133 |
+
print(f"\n--- Running quantization: {args.output} ---")
|
| 134 |
+
success = run_quantization(flags, args.model, args.output)
|
| 135 |
+
if success:
|
| 136 |
+
print("Done!")
|
| 137 |
+
else:
|
| 138 |
+
print("Failed!")
|
| 139 |
+
sys.exit(1)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def _show_tier_summary(assignments, imp_table, ne_map, padded_ne_map=None):
|
| 143 |
+
stats = compute_stats(assignments, ne_map, padded_ne_map)
|
| 144 |
+
|
| 145 |
+
print("\n Tier distribution:")
|
| 146 |
+
for tier in sorted(stats["by_tier_count"].keys()):
|
| 147 |
+
count = stats["by_tier_count"][tier]
|
| 148 |
+
mib = stats["by_tier_mib"].get(tier, 0.0)
|
| 149 |
+
print(f" {tier}: {count} tensors ({mib:.1f} MiB)")
|
| 150 |
+
print(f" Total estimated size: {stats['total_mib']:.1f} MiB")
|
| 151 |
+
|
| 152 |
+
ranked = sorted(
|
| 153 |
+
[(n, v) for n, v in imp_table.items()],
|
| 154 |
+
key=lambda x: -x[1]["importance_mean"],
|
| 155 |
+
)
|
| 156 |
+
print("\n Top 10 by importance:")
|
| 157 |
+
for n, v in ranked[:10]:
|
| 158 |
+
tier = assignments.get(n, "base")
|
| 159 |
+
display = n.replace(".weight", "").replace(".bias", "")
|
| 160 |
+
print(f" {display[:52]:52s} imp={v['importance_mean']:10.0f} tier={tier}")
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def _show_size_result(dry_size, target_mib):
|
| 164 |
+
if dry_size:
|
| 165 |
+
print(f" Estimated size: {dry_size:.0f} MiB ({dry_size / 1024:.2f} GB)")
|
| 166 |
+
diff = dry_size - target_mib
|
| 167 |
+
if diff > 0:
|
| 168 |
+
print(f" ⚠ Over target by {diff:.0f} MiB")
|
| 169 |
+
else:
|
| 170 |
+
print(f" ✓ Under target by {-diff:.0f} MiB")
|
| 171 |
+
else:
|
| 172 |
+
print(" ⚠ Could not parse size from dry-run output")
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def _show_floors():
|
| 176 |
+
print(" Class hard floors (never below without --allow-q3-or-lower):\n")
|
| 177 |
+
max_n = max(len(c) for c in CLASS_HARD_FLOORS)
|
| 178 |
+
for cls, floor in sorted(CLASS_HARD_FLOORS.items()):
|
| 179 |
+
print(f" {cls:<{max_n}} → {floor}")
|
| 180 |
+
print(f"\n Default floor (unknown class): Q4_K")
|
| 181 |
+
print(f" --allow-q3-or-lower enables Q3_K for: ffn_down, attn_output, ssm_out")
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
if __name__ == "__main__":
|
| 185 |
+
main()
|
model_reader.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gguf
|
| 2 |
+
import numpy as np
|
| 3 |
+
from constants import ARCH_FEATURES
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def detect_architecture(tensors: dict) -> str:
|
| 7 |
+
"""Detect model architecture from tensor names."""
|
| 8 |
+
names = list(tensors.keys())
|
| 9 |
+
|
| 10 |
+
has_ssm = any("ssm_" in n for n in names)
|
| 11 |
+
has_qkv = any("attn_qkv" in n for n in names)
|
| 12 |
+
has_nextn = any("nextn" in n for n in names)
|
| 13 |
+
has_moe = any("exps" in n for n in names)
|
| 14 |
+
has_separate_qkv = any("attn_q.weight" in n for n in names)
|
| 15 |
+
has_gemma_specific = any(
|
| 16 |
+
t in n for t in ("layer_output_scale", "post_attention_norm", "post_ffw_norm")
|
| 17 |
+
for n in names
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
if has_ssm and has_qkv:
|
| 21 |
+
return "qwen35"
|
| 22 |
+
if has_moe:
|
| 23 |
+
return "mellum2"
|
| 24 |
+
# Исправление: заменяем неопределённую has_blk_attn на уже существующий признак
|
| 25 |
+
if has_separate_qkv or has_gemma_specific:
|
| 26 |
+
return "gemma4"
|
| 27 |
+
return "unknown"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _detect_prefix(tensors: dict) -> str:
|
| 31 |
+
for name in tensors:
|
| 32 |
+
if name.startswith("BLK."):
|
| 33 |
+
return "BLK"
|
| 34 |
+
return "blk"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _estimate_layers(tensors: dict) -> int:
|
| 38 |
+
max_layer = 0
|
| 39 |
+
for name in tensors:
|
| 40 |
+
parts = name.split(".")
|
| 41 |
+
if len(parts) >= 2 and parts[0] in ("blk", "BLK"):
|
| 42 |
+
try:
|
| 43 |
+
layer = int(parts[1])
|
| 44 |
+
if layer > max_layer:
|
| 45 |
+
max_layer = layer
|
| 46 |
+
except ValueError:
|
| 47 |
+
pass
|
| 48 |
+
return max_layer + 1 # layers are 0-indexed
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def read_model(path: str) -> dict:
|
| 52 |
+
"""Parse BF16 GGUF, return model info."""
|
| 53 |
+
r = gguf.GGUFReader(path)
|
| 54 |
+
tensors = {}
|
| 55 |
+
meta = {}
|
| 56 |
+
|
| 57 |
+
for k, v in r.fields.items():
|
| 58 |
+
# Безопасное извлечение данных (список/число, а не raw numpy)
|
| 59 |
+
try:
|
| 60 |
+
data = v.data
|
| 61 |
+
if isinstance(data, np.ndarray):
|
| 62 |
+
data = data.tolist()
|
| 63 |
+
elif isinstance(data, (np.generic,)):
|
| 64 |
+
data = data.item()
|
| 65 |
+
meta[k] = data
|
| 66 |
+
except Exception:
|
| 67 |
+
meta[k] = str(v)
|
| 68 |
+
|
| 69 |
+
for t in r.tensors:
|
| 70 |
+
shape = list(t.shape)
|
| 71 |
+
name = t.name
|
| 72 |
+
n_elements = int(np.prod(shape))
|
| 73 |
+
tensors[name] = {
|
| 74 |
+
"shape": shape,
|
| 75 |
+
"n_elements": n_elements,
|
| 76 |
+
"size_mib": n_elements * 2 / 1024 / 1024,
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
arch = detect_architecture(tensors)
|
| 80 |
+
arch_features = ARCH_FEATURES.get(arch, {}).copy()
|
| 81 |
+
|
| 82 |
+
prefix = _detect_prefix(tensors)
|
| 83 |
+
n_layers = _estimate_layers(tensors)
|
| 84 |
+
|
| 85 |
+
if arch == "mellum2" and arch_features.get("moe_intermediate_size", 0) == 0:
|
| 86 |
+
arch_features["moe_intermediate_size"] = 896
|
| 87 |
+
|
| 88 |
+
arch_features["prefix"] = prefix
|
| 89 |
+
if n_layers > 0:
|
| 90 |
+
arch_features["n_layers"] = n_layers
|
| 91 |
+
|
| 92 |
+
has_moe = any("exps" in n for n in tensors)
|
| 93 |
+
if has_moe:
|
| 94 |
+
arch_features["has_moe"] = True
|
| 95 |
+
|
| 96 |
+
has_nextn = any("nextn" in n for n in tensors)
|
| 97 |
+
# blk.32 is MTP only in ~32-layer models (Qwen). Skip for deeper models (Gemma4, 48 layers).
|
| 98 |
+
has_blk32 = n_layers <= 33 and any(
|
| 99 |
+
n.startswith("blk.32.") or n.startswith("BLK.32.") for n in tensors
|
| 100 |
+
)
|
| 101 |
+
arch_features["has_mtp"] = has_nextn or has_blk32
|
| 102 |
+
|
| 103 |
+
return {
|
| 104 |
+
"path": path,
|
| 105 |
+
"architecture": arch,
|
| 106 |
+
"features": arch_features,
|
| 107 |
+
"tensors": tensors,
|
| 108 |
+
"n_tensors": len(tensors),
|
| 109 |
+
"meta": meta,
|
| 110 |
+
}
|
quantizer.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
import re
|
| 3 |
+
import os
|
| 4 |
+
from utils import parse_size_line, parse_quant_size
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
QUANTIZER_PATH = "/home/maxyag27/llm-tools/llama.cpp/build/bin/llama-quantize"
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def _build_cmd(flags: dict, model_in: str, model_out: str, dry_run: bool = False) -> list:
|
| 11 |
+
"""Build llama-quantize command. Options before positional args."""
|
| 12 |
+
cmd = [QUANTIZER_PATH]
|
| 13 |
+
|
| 14 |
+
if dry_run:
|
| 15 |
+
cmd.append("--dry-run")
|
| 16 |
+
|
| 17 |
+
if flags.get("imatrix"):
|
| 18 |
+
imatrix = flags["imatrix"]
|
| 19 |
+
if isinstance(imatrix, list):
|
| 20 |
+
imatrix = imatrix[0] # llama-quantize accepts one imatrix; we combined in importance table
|
| 21 |
+
cmd.extend(["--imatrix", imatrix])
|
| 22 |
+
cmd.extend(["--output-tensor-type", flags["output_tensor_type"]])
|
| 23 |
+
cmd.extend(["--token-embedding-type", flags["token_embedding_type"]])
|
| 24 |
+
|
| 25 |
+
for rule in flags["tensor_type_rules"]:
|
| 26 |
+
parts = rule.split(" ", 1)
|
| 27 |
+
if len(parts) == 2:
|
| 28 |
+
cmd.extend(["--tensor-type", parts[1].strip('"')])
|
| 29 |
+
|
| 30 |
+
# Positional args: model_in model_out type
|
| 31 |
+
cmd.append(model_in)
|
| 32 |
+
cmd.append(model_out)
|
| 33 |
+
cmd.append(flags["base_type"])
|
| 34 |
+
|
| 35 |
+
return cmd
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def run_dry_run(flags: dict, model_in: str) -> float | None:
|
| 39 |
+
"""Run llama-quantize --dry-run and return quant size in MiB."""
|
| 40 |
+
cmd = _build_cmd(flags, model_in, "/dev/null", dry_run=True)
|
| 41 |
+
|
| 42 |
+
result = subprocess.run(
|
| 43 |
+
cmd,
|
| 44 |
+
capture_output=True,
|
| 45 |
+
text=True,
|
| 46 |
+
timeout=300,
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
output = (result.stdout or "") + (result.stderr or "")
|
| 50 |
+
size = parse_quant_size(output)
|
| 51 |
+
if size is not None:
|
| 52 |
+
return size
|
| 53 |
+
|
| 54 |
+
print("STDERR:", result.stderr[:2000])
|
| 55 |
+
return None
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def run_quantization(flags: dict, model_in: str, model_out: str, dry_run: bool = False) -> bool:
|
| 59 |
+
"""Run actual quantization or dry run."""
|
| 60 |
+
cmd = _build_cmd(flags, model_in, model_out, dry_run=dry_run)
|
| 61 |
+
|
| 62 |
+
print("Running:", " ".join(cmd[:6]) + " ...")
|
| 63 |
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600)
|
| 64 |
+
|
| 65 |
+
if dry_run:
|
| 66 |
+
output = (result.stdout or "") + (result.stderr or "")
|
| 67 |
+
return parse_quant_size(output)
|
| 68 |
+
|
| 69 |
+
print(result.stderr[:500])
|
| 70 |
+
success = result.returncode == 0
|
| 71 |
+
if success:
|
| 72 |
+
import os
|
| 73 |
+
size_mib = os.path.getsize(model_out) / 1024 / 1024
|
| 74 |
+
print(f"Done: {model_out} ({size_mib:.0f} MiB)")
|
| 75 |
+
return success
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gguf>=0.10.0
|
| 2 |
+
numpy>=1.24.0
|
utils.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from constants import GGUF_TYPE_NAMES, GGUF_TYPE_NAMES_INV
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def type_name(type_id: int) -> str:
|
| 6 |
+
return GGUF_TYPE_NAMES.get(type_id, f"UNKNOWN({type_id})")
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def type_id(name: str) -> int:
|
| 10 |
+
return GGUF_TYPE_NAMES_INV.get(name, -1)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def parse_size_line(line: str) -> float | None:
|
| 14 |
+
import re
|
| 15 |
+
m = re.search(r"quant size\s*=\s*([0-9.]+)\s*MiB", line)
|
| 16 |
+
if m:
|
| 17 |
+
return float(m.group(1))
|
| 18 |
+
m = re.search(r"model size\s*=\s*([0-9.]+)\s*MiB", line)
|
| 19 |
+
if m:
|
| 20 |
+
return float(m.group(1))
|
| 21 |
+
return None
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def parse_quant_size(output: str) -> float | None:
|
| 25 |
+
"""Extract quant size from full output (prefers quant over model size)."""
|
| 26 |
+
import re
|
| 27 |
+
# Try quant size first across all lines
|
| 28 |
+
m = re.search(r"quant size\s*=\s*([0-9.]+)\s*MiB", output)
|
| 29 |
+
if m:
|
| 30 |
+
return float(m.group(1))
|
| 31 |
+
# Fall back to model size
|
| 32 |
+
m = re.search(r"model size\s*=\s*([0-9.]+)\s*MiB", output)
|
| 33 |
+
if m:
|
| 34 |
+
return float(m.group(1))
|
| 35 |
+
return None
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def parse_fallback_warnings(output: str) -> int:
|
| 39 |
+
import re
|
| 40 |
+
return len(re.findall(r"converting to\s+(q[0-9]_[0-9KMS]|iq[0-9])", output))
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def format_size(mib: float) -> str:
|
| 44 |
+
if mib >= 1024:
|
| 45 |
+
return f"{mib/1024:.2f} GB"
|
| 46 |
+
return f"{mib:.0f} MiB"
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def get_tensor_type(tensor_name: str) -> str:
|
| 50 |
+
parts = tensor_name.split(".")
|
| 51 |
+
if len(parts) >= 2 and parts[0] == "blk":
|
| 52 |
+
return parts[2] if len(parts) >= 3 else "unknown"
|
| 53 |
+
return "global"
|