|
| 1 | +# ml8-4 Weight Calibration |
| 2 | + |
| 3 | +MAD-223 Phase B. End-to-end tooling to quantize a HuggingFace model's MLP weights |
| 4 | +to **ml8-4** format: 16 signed centroids per group, ~4.125 bpv. |
| 5 | + |
| 6 | +For KV-cache calibration (MAD-214), see the existing `README.md` in this directory. |
| 7 | + |
| 8 | +## Pipeline overview |
| 9 | + |
| 10 | +``` |
| 11 | +HF model (fp16) ──┐ |
| 12 | + │ calibrate_ml8.py |
| 13 | +wikitext-2 ───────┤ ────────────────────► per-layer .pt blobs + manifest.json |
| 14 | + │ (Lloyd-Max + GPTQ) │ |
| 15 | + │ │ |
| 16 | + │ ▼ reconstruct_model.py |
| 17 | + │ ml8-overlaid HF model + PPL re-eval |
| 18 | + │ |
| 19 | + └─► diagnose_calibration.py (compare quant variants) |
| 20 | +``` |
| 21 | + |
| 22 | +## Scripts |
| 23 | + |
| 24 | +| Script | Purpose | |
| 25 | +|---|---| |
| 26 | +| `centroid_quantizer.py` | `CentroidQuantizer` — drop-in for auto_gptq.Quantizer (signed-16 LUT, MSE Lloyd-Max, optional mag_weighted / Hessian-aware) | |
| 27 | +| `calibrate_ml8.py` | Main driver: HF model + wikitext-2 → per-layer GPTQ-quantized .pt blobs + manifest | |
| 28 | +| `diagnose_calibration.py` | Runs 5 quantization variants on the same matrix (uniform INT4, naive ± loss, GPTQ ± loss) for debugging | |
| 29 | +| `ml8_io.py` | `load_ml8_layer`, `reconstruct_weight`, `bits_per_value` — disk format helpers | |
| 30 | +| `reconstruct_model.py` | Load HF model + overlay all saved .pt blobs → re-eval PPL on rehydrated model | |
| 31 | +| `calibration_report.py` | Pretty-print `manifest.json` — Y_SNR distribution, per-kind, size, PPL, worst layers | |
| 32 | + |
| 33 | +## Quick start |
| 34 | + |
| 35 | +```bash |
| 36 | +# 1. Calibrate (≈45 min for Qwen3.5-4B on R9700) |
| 37 | +python3 calibrate_ml8.py \ |
| 38 | + --model Qwen/Qwen3.5-4B \ |
| 39 | + --output-dir /tmp/ml8-qwen3-4b \ |
| 40 | + --n-samples 32 --seq-len 1024 --group-size 128 \ |
| 41 | + --eval-ppl --ppl-max-tokens 100000 |
| 42 | + |
| 43 | +# 2. Summary |
| 44 | +python3 calibration_report.py /tmp/ml8-qwen3-4b |
| 45 | + |
| 46 | +# 3. Re-eval PPL from saved artifacts (verifies disk format) |
| 47 | +python3 reconstruct_model.py \ |
| 48 | + --model Qwen/Qwen3.5-4B \ |
| 49 | + --calibration-dir /tmp/ml8-qwen3-4b \ |
| 50 | + --eval-ppl --also-eval-baseline --ppl-max-tokens 100000 |
| 51 | +``` |
| 52 | + |
| 53 | +## Defaults (and why) |
| 54 | + |
| 55 | +| Knob | Default | Rationale | |
| 56 | +|---|---|---| |
| 57 | +| `--fit-loss` | `mse` | Lloyd-Max with uniform sample weighting. **Do NOT use `mag_weighted p=5`** — it was the MAD-214 KV winner because activations have heavy outliers; weights are Gaussian-near-zero and mag_p5 starves the dense center (measured: 10 dB regression on Qwen3.5-4B). | |
| 58 | +| `--n-centroids` | 16 | 4-bit indices (ml8-**4**). | |
| 59 | +| `--group-size` | 128 | Matches auto-gptq convention. Smaller = more LUT overhead but better fit; 128 is the sweet spot. | |
| 60 | +| `--percdamp` | 0.01 | Hessian diagonal damping for Cholesky stability. | |
| 61 | +| `--n-samples` | 64 | Calibration corpus size in wikitext rows. 32 also works; 128 is overkill. | |
| 62 | +| `--seq-len` | 2048 | Per-row tokenization length. Combined with `--n-samples`, target ≈30-130K total calibration tokens. | |
| 63 | + |
| 64 | +## Metrics |
| 65 | + |
| 66 | +Two SNR metrics are reported per linear: |
| 67 | + |
| 68 | +- **W_SNR** — element-wise weight reconstruction (what naive snap optimizes) |
| 69 | +- **Y_SNR** — output-space reconstruction weighted by H (what GPTQ optimizes) ← **the meaningful one** |
| 70 | + |
| 71 | +GPTQ deliberately makes individual weight elements DIFFER MORE from the original to make the OUTPUT activations MATCH MORE closely. Expect GPTQ to LOSE on W_SNR and WIN big on Y_SNR. Typical Qwen-class MLP linears: Y_SNR 26–30 dB with MSE+GPTQ. |
| 72 | + |
| 73 | +## File format (interim, before MAD-223 Phase C) |
| 74 | + |
| 75 | +Each layer's `.pt` blob: |
| 76 | + |
| 77 | +```python |
| 78 | +{ |
| 79 | + "name": str, # e.g. "model.layers.0.mlp.up_proj" |
| 80 | + "shape": [rows, in_features], |
| 81 | + "group_size": int, # 128 |
| 82 | + "n_centroids": int, # 16 |
| 83 | + "indices": int8 [rows, in_features], # values 0..15 |
| 84 | + "centroids_per_group": fp32 [n_groups, 16], |
| 85 | + "scale_per_group": fp32 [rows, n_groups], |
| 86 | + "mse": float, "w_snr_db": float, "y_snr_db": float, "rel_err": float, |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +Reconstruction: `W[r, c] = centroids_per_group[c // group_size][indices[r, c]] * scale_per_group[r, c // group_size]` |
| 91 | + |
| 92 | +Phase C will define the native `.ml8` binary format + GGUF-wrapped variant (per the |
| 93 | +decision to ship both formats — see saved KG decision 2026-05-22). |
| 94 | + |
| 95 | +## Acceptance gate (MAD-223) |
| 96 | + |
| 97 | +`Δ_PPL = quantized − baseline` measured on wikitext-2 test split. |
| 98 | + |
| 99 | +- **`Δ_PPL < 0.08`** → ship as-is (Phase B.5 AWQ scaling NOT needed) |
| 100 | +- **`Δ_PPL ≥ 0.08`** → enable Phase B.5 AWQ activation-scaling preprocess |
| 101 | + |
| 102 | +`calibrate_ml8.py --eval-ppl` and `calibration_report.py` both check this automatically. |
0 commit comments