Upload lattice_repair.py
Browse files- lattice_repair.py +711 -0
lattice_repair.py
ADDED
|
@@ -0,0 +1,711 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""lattice_repair - whole-lattice cartography + drag of a quantized GGUF onto its F16/BF16 source.
|
| 3 |
+
|
| 4 |
+
Sibling of lattice_drag.py: same chunked, weight-aware, byte-surgical
|
| 5 |
+
machinery (frames frozen, only outlier codes move, everything else stays
|
| 6 |
+
byte-for-byte identical), extended to the WHOLE lattice:
|
| 7 |
+
|
| 8 |
+
1. Whole lattice: every tensor of the quant GGUF is enumerated and matched
|
| 9 |
+
to the source GGUF by name - the Q2_K/Q3_K/Q4_0 tensors are worked on;
|
| 10 |
+
all other tensors are measured against the source and dumped too (F32/F16/BF16
|
| 11 |
+
compared directly; other quant types such as Q8_0/Q6_K are dequantized, so
|
| 12 |
+
their real displacement is reported, not assumed zero).
|
| 13 |
+
2. Position difference per weight: for every weight the displacement
|
| 14 |
+
d[i] = source[i] - dequantized[i] between its position on the source
|
| 15 |
+
lattice and its position on the quant lattice is computed and dumped
|
| 16 |
+
in full, BEFORE the drag (.delta.before.f16) and AFTER (.delta.after.f16).
|
| 17 |
+
3. The drag (the pull): outlier weights (|d| > --thresh code steps of
|
| 18 |
+
their sub-block) are moved to the nearest code their EXISTING block
|
| 19 |
+
frame allows, only when that reduces |d| - never regress; scales,
|
| 20 |
+
minima, fp16 d values and non-outlier codes stay untouched.
|
| 21 |
+
Every moved weight goes to the --catalog CSV with its source position,
|
| 22 |
+
old/new lattice positions and old/new codes.
|
| 23 |
+
|
| 24 |
+
Dumps per tensor (float16 raw files): <name>.src.f16 (source lattice),
|
| 25 |
+
<name>.qnt.f16 (quant lattice after the drag), <name>.delta.before.f16 /
|
| 26 |
+
<name>.delta.after.f16 (per-weight displacement before / after).
|
| 27 |
+
Kernel/matrix ops add-ons (rope_freq etc.) and all metadata ride along
|
| 28 |
+
unchanged: output is a copy of the quant GGUF with data regions patched
|
| 29 |
+
in place - loads directly in llama.cpp.
|
| 30 |
+
|
| 31 |
+
python3 lattice_repair.py --source model-f16.gguf --quant model-Q3.gguf \\
|
| 32 |
+
--output model-Q3-repaired.gguf --thresh 1.0 --catalog moved.csv
|
| 33 |
+
|
| 34 |
+
Use --dry-run first: it reports everything and writes nothing.
|
| 35 |
+
--dump-dir writes the full source and quant lattices (large).
|
| 36 |
+
|
| 37 |
+
Requires: numpy, gguf (pip install gguf).
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
from __future__ import annotations
|
| 41 |
+
|
| 42 |
+
import argparse
|
| 43 |
+
import shutil
|
| 44 |
+
import sys
|
| 45 |
+
import time
|
| 46 |
+
from pathlib import Path
|
| 47 |
+
|
| 48 |
+
import numpy as np
|
| 49 |
+
|
| 50 |
+
# the pip gguf package; local gguf.py must not shadow it
|
| 51 |
+
sys.path = [p for p in sys.path if p not in ("", str(Path.cwd()))]
|
| 52 |
+
from gguf import GGMLQuantizationType as QT
|
| 53 |
+
from gguf import GGML_QUANT_SIZES, GGUFReader
|
| 54 |
+
from gguf import quants as gq
|
| 55 |
+
|
| 56 |
+
# bytes per block, elements per block, code range
|
| 57 |
+
FORMATS = {
|
| 58 |
+
QT.Q2_K: dict(name="Q2_K", be=256, bb=84, cmin=0, cmax=3),
|
| 59 |
+
QT.Q3_K: dict(name="Q3_K", be=256, bb=110, cmin=-4, cmax=3),
|
| 60 |
+
QT.Q4_0: dict(name="Q4_0", be=32, bb=18, cmin=0, cmax=15),
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# ---------------------------------------------------------------------------
|
| 65 |
+
# block layouts. Every format is expressed as deq = scale * code + offset
|
| 66 |
+
# per element, so one drag routine serves all three.
|
| 67 |
+
# ---------------------------------------------------------------------------
|
| 68 |
+
def _f16(b):
|
| 69 |
+
return np.ascontiguousarray(b).view("<f2").reshape(-1).astype(np.float32)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def unpack_q2k(raw):
|
| 73 |
+
B = raw.shape[0]
|
| 74 |
+
sc, qs = raw[:, :16], raw[:, 16:80]
|
| 75 |
+
d, dmin = _f16(raw[:, 80:82]), _f16(raw[:, 82:84])
|
| 76 |
+
codes = np.empty((B, 256), np.int16)
|
| 77 |
+
for half in range(2):
|
| 78 |
+
q = qs[:, 32 * half : 32 * half + 32]
|
| 79 |
+
for j in range(4):
|
| 80 |
+
codes[:, 128 * half + 32 * j : 128 * half + 32 * j + 32] = (q >> (2 * j)) & 3
|
| 81 |
+
scale = d[:, None] * (sc & 15).astype(np.float32)
|
| 82 |
+
off = -dmin[:, None] * (sc >> 4).astype(np.float32)
|
| 83 |
+
return codes, np.repeat(scale, 16, 1), np.repeat(off, 16, 1)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def pack_q2k(raw, codes):
|
| 87 |
+
B = raw.shape[0]
|
| 88 |
+
qs = np.zeros((B, 64), np.uint8)
|
| 89 |
+
for half in range(2):
|
| 90 |
+
for j in range(4):
|
| 91 |
+
c = codes[:, 128 * half + 32 * j : 128 * half + 32 * j + 32].astype(np.uint8)
|
| 92 |
+
qs[:, 32 * half : 32 * half + 32] |= c << (2 * j)
|
| 93 |
+
out = raw.copy()
|
| 94 |
+
out[:, 16:80] = qs
|
| 95 |
+
return out
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _q3k_scales(s12):
|
| 99 |
+
s = s12.astype(np.int32)
|
| 100 |
+
out = np.empty((s.shape[0], 16), np.int32)
|
| 101 |
+
for k in range(4):
|
| 102 |
+
out[:, k] = (s[:, k] & 15) | (((s[:, 8 + k] >> 0) & 3) << 4)
|
| 103 |
+
out[:, 4 + k] = (s[:, 4 + k] & 15) | (((s[:, 8 + k] >> 2) & 3) << 4)
|
| 104 |
+
out[:, 8 + k] = (s[:, k] >> 4) | (((s[:, 8 + k] >> 4) & 3) << 4)
|
| 105 |
+
out[:, 12 + k] = (s[:, 4 + k] >> 4) | (((s[:, 8 + k] >> 6) & 3) << 4)
|
| 106 |
+
return out - 32
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def unpack_q3k(raw):
|
| 110 |
+
B = raw.shape[0]
|
| 111 |
+
hm, qs = raw[:, :32], raw[:, 32:96]
|
| 112 |
+
sc = _q3k_scales(raw[:, 96:108])
|
| 113 |
+
d = _f16(raw[:, 108:110])
|
| 114 |
+
codes = np.empty((B, 256), np.int16)
|
| 115 |
+
for g in range(8):
|
| 116 |
+
low = (qs[:, 32 * (g // 4) : 32 * (g // 4) + 32] >> (2 * (g % 4))) & 3
|
| 117 |
+
hi = (hm >> g) & 1
|
| 118 |
+
codes[:, 32 * g : 32 * g + 32] = low.astype(np.int16) + 4 * hi.astype(np.int16) - 4
|
| 119 |
+
scale = d[:, None] * sc.astype(np.float32)
|
| 120 |
+
return codes, np.repeat(scale, 16, 1), np.zeros((B, 256), np.float32)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def pack_q3k(raw, codes):
|
| 124 |
+
B = raw.shape[0]
|
| 125 |
+
u = (codes.astype(np.int16) + 4).astype(np.uint8)
|
| 126 |
+
hm = np.zeros((B, 32), np.uint8)
|
| 127 |
+
qs = np.zeros((B, 64), np.uint8)
|
| 128 |
+
for g in range(8):
|
| 129 |
+
ug = u[:, 32 * g : 32 * g + 32]
|
| 130 |
+
qs[:, 32 * (g // 4) : 32 * (g // 4) + 32] |= (ug & 3) << (2 * (g % 4))
|
| 131 |
+
hm |= (ug >> 2) << g
|
| 132 |
+
out = raw.copy()
|
| 133 |
+
out[:, :32] = hm
|
| 134 |
+
out[:, 32:96] = qs
|
| 135 |
+
return out
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def unpack_q4_0(raw):
|
| 139 |
+
B = raw.shape[0]
|
| 140 |
+
d = _f16(raw[:, 0:2])
|
| 141 |
+
qs = raw[:, 2:18]
|
| 142 |
+
codes = np.empty((B, 32), np.int16)
|
| 143 |
+
codes[:, :16] = qs & 15
|
| 144 |
+
codes[:, 16:] = qs >> 4
|
| 145 |
+
scale = np.repeat(d[:, None], 32, 1)
|
| 146 |
+
return codes, scale, -8.0 * scale
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def pack_q4_0(raw, codes):
|
| 150 |
+
u = codes.astype(np.uint8)
|
| 151 |
+
out = raw.copy()
|
| 152 |
+
out[:, 2:18] = u[:, :16] | (u[:, 16:] << 4)
|
| 153 |
+
return out
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
UNPACK = {QT.Q2_K: unpack_q2k, QT.Q3_K: unpack_q3k, QT.Q4_0: unpack_q4_0}
|
| 157 |
+
PACK = {QT.Q2_K: pack_q2k, QT.Q3_K: pack_q3k, QT.Q4_0: pack_q4_0}
|
| 158 |
+
|
| 159 |
+
# ---------------------------------------------------------------------------
|
| 160 |
+
# the drag
|
| 161 |
+
# ---------------------------------------------------------------------------
|
| 162 |
+
def drag_chunk(ftype, raw, x, thresh):
|
| 163 |
+
"""Return (new_raw, stats, moved_mask, deq_before, deq_after, codes_old, codes_new)."""
|
| 164 |
+
f = FORMATS[ftype]
|
| 165 |
+
codes, s, o = UNPACK[ftype](raw)
|
| 166 |
+
deq = s * codes + o
|
| 167 |
+
e = x - deq
|
| 168 |
+
step = np.abs(s)
|
| 169 |
+
live = step > 0
|
| 170 |
+
outlier = live & (np.abs(e) > thresh * step)
|
| 171 |
+
|
| 172 |
+
safe = np.where(live, s, 1.0)
|
| 173 |
+
near = np.clip(np.rint((x - o) / safe), f["cmin"], f["cmax"]).astype(np.int16)
|
| 174 |
+
new_deq = s * near + o
|
| 175 |
+
new_e = x - new_deq
|
| 176 |
+
move = outlier & (np.abs(new_e) < np.abs(e))
|
| 177 |
+
codes2 = np.where(move, near, codes)
|
| 178 |
+
deq2 = np.where(move, new_deq, deq)
|
| 179 |
+
e2 = x - deq2
|
| 180 |
+
|
| 181 |
+
flim = (outlier & (np.abs(e2) > thresh * step)).any(axis=1) # (B,)
|
| 182 |
+
st = dict(
|
| 183 |
+
weights=x.size,
|
| 184 |
+
outliers=int(outlier.sum()),
|
| 185 |
+
moved=int(move.sum()),
|
| 186 |
+
frame_limited=int((outlier & (np.abs(e2) > thresh * step)).sum()),
|
| 187 |
+
blocks_frame_limited=int(flim.sum()),
|
| 188 |
+
dead_frame=int((~live).sum()),
|
| 189 |
+
sse_before=float(np.sum(e.astype(np.float64) ** 2)),
|
| 190 |
+
sse_after=float(np.sum(e2.astype(np.float64) ** 2)),
|
| 191 |
+
max_before=float(np.max(np.abs(e))) if e.size else 0.0,
|
| 192 |
+
max_after=float(np.max(np.abs(e2))) if e.size else 0.0,
|
| 193 |
+
dc_before=float(np.sum(np.abs(e.sum(axis=1)))),
|
| 194 |
+
dc_after=float(np.sum(np.abs(e2.sum(axis=1)))),
|
| 195 |
+
blocks_changed=int(move.any(axis=1).sum()),
|
| 196 |
+
blocks=raw.shape[0],
|
| 197 |
+
)
|
| 198 |
+
new_raw = PACK[ftype](raw, codes2) if st["moved"] else raw
|
| 199 |
+
return new_raw, st, move, flim, deq, deq2, codes, codes2
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def stats_add(into, st):
|
| 203 |
+
if into is None:
|
| 204 |
+
return dict(st)
|
| 205 |
+
for k, v in st.items():
|
| 206 |
+
into[k] = max(into.get(k, 0.0), v) if k.startswith("max_") else into.get(k, 0) + v
|
| 207 |
+
return into
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
# ---------------------------------------------------------------------------
|
| 211 |
+
# frame re-fit for frame-limited outliers (--fix-frames).
|
| 212 |
+
# Only blocks that still hold frame-limited outlier weights are touched.
|
| 213 |
+
# Each block's frame (d, sc) is re-solved on its own grid from the SOURCE:
|
| 214 |
+
# closed-form least squares over the existing codes plus an fp16-ULP window
|
| 215 |
+
# probe; codes are optionally re-projected to the new frame. A candidate is
|
| 216 |
+
# accepted per block ONLY when it lowers the block cost SSE + dc_lambda*DC^2;
|
| 217 |
+
# every non-accepting block and every untouched block stays byte-identical.
|
| 218 |
+
# ---------------------------------------------------------------------------
|
| 219 |
+
def _bits16(v):
|
| 220 |
+
"""float values -> fp16 bit patterns (uint16)."""
|
| 221 |
+
return np.asarray(v, np.float32).astype(np.float16).view("<u2").astype(np.uint16)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def _val16(b):
|
| 225 |
+
"""fp16 bit patterns (uint16) -> float32 values (same shape)."""
|
| 226 |
+
return np.ascontiguousarray(b).view("<f2").astype(np.float32)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def _ulp_window(b, rad):
|
| 230 |
+
"""fp16 bit windows +-rad ULPs around the magnitudes of patterns b."""
|
| 231 |
+
mag = (b & 0x7FFF).astype(np.int32)
|
| 232 |
+
win = np.clip(mag[:, None] + np.arange(-rad, rad + 1), 0, 0x7BFF).astype(np.uint16)
|
| 233 |
+
return win | (b & 0x8000)[:, None]
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def _better(cost_new, cost_old):
|
| 237 |
+
"""strictly better beyond float noise (relative 1e-9)."""
|
| 238 |
+
return cost_new < cost_old * (1.0 - 1e-9) - 1e-30
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def _block_cost(e, dc_lam):
|
| 242 |
+
"""per-block cost: SSE + dc_lam * DC^2 (reduced over the last axis)."""
|
| 243 |
+
e = np.asarray(e, np.float64)
|
| 244 |
+
dc = e.sum(axis=-1)
|
| 245 |
+
cost = np.sum(e * e, axis=-1) + dc_lam * dc * dc
|
| 246 |
+
return np.where(np.isfinite(cost), cost, np.inf)
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def refit_q4_0(raw, x, dc_lam, rad):
|
| 250 |
+
"""d re-solve on Q4_0 blocks; codes optionally re-projected. Returns
|
| 251 |
+
(new_raw, improved_mask, deq). Never mutates the input."""
|
| 252 |
+
codes, s, o = unpack_q4_0(raw)
|
| 253 |
+
d16 = np.ascontiguousarray(raw[:, 0] | (raw[:, 1].astype(np.uint16) << 8))
|
| 254 |
+
cp = codes.astype(np.float64) - 8.0 # deq = d * cp
|
| 255 |
+
B = raw.shape[0]
|
| 256 |
+
deq0 = s * codes + o
|
| 257 |
+
cost0 = _block_cost(x - deq0, dc_lam)
|
| 258 |
+
|
| 259 |
+
win_bits = _ulp_window(d16, rad) # (B, W)
|
| 260 |
+
Sxc = np.sum(x.astype(np.float64) * cp, axis=1)
|
| 261 |
+
Scc = np.sum(cp * cp, axis=1)
|
| 262 |
+
d_ls = np.where(Scc > 1e-30, Sxc / np.maximum(Scc, 1e-30), 0.0)
|
| 263 |
+
b_ls = np.where((d_ls > 0) & (d_ls < 6e4), _bits16(d_ls), 0)
|
| 264 |
+
all_bits = np.concatenate([win_bits, b_ls[:, None]], axis=1) # (B, W+1)
|
| 265 |
+
dv = _val16(all_bits)
|
| 266 |
+
dv = np.where(dv > 0, dv, np.inf) # invalid candidates lose
|
| 267 |
+
W = all_bits.shape[1]
|
| 268 |
+
|
| 269 |
+
deq_keep = dv[:, :, None] * cp[:, None, :]
|
| 270 |
+
cost_keep = _block_cost(x[:, None, :] - deq_keep, dc_lam)
|
| 271 |
+
u_ref = np.clip(np.rint(x[:, None, :] / dv[:, :, None] + 8.0), 0, 15)
|
| 272 |
+
deq_ref = dv[:, :, None] * (u_ref - 8.0)
|
| 273 |
+
cost_ref = _block_cost(x[:, None, :] - deq_ref, dc_lam)
|
| 274 |
+
# choice table: candidates x [keep codes, refit codes]
|
| 275 |
+
table_bits = np.concatenate([all_bits, all_bits], axis=1) # (B, 2W)
|
| 276 |
+
table_cost = np.concatenate([cost_keep, cost_ref], axis=1)
|
| 277 |
+
pick = np.argmin(table_cost, axis=1)
|
| 278 |
+
cost_new = table_cost[np.arange(B), pick]
|
| 279 |
+
mode_ref = pick >= W
|
| 280 |
+
within = pick % W
|
| 281 |
+
improved = _better(cost_new, cost0)
|
| 282 |
+
rows = np.where(improved)[0]
|
| 283 |
+
if rows.size == 0:
|
| 284 |
+
return raw, improved, deq0
|
| 285 |
+
deq = deq0.copy()
|
| 286 |
+
deq[rows] = np.where(mode_ref[rows, None],
|
| 287 |
+
deq_ref[rows, within[rows]],
|
| 288 |
+
deq_keep[rows, within[rows]])
|
| 289 |
+
out = raw.copy()
|
| 290 |
+
out[rows, :2] = table_bits[np.arange(B), pick][rows].astype("<u2").view(np.uint8).reshape(-1, 2)
|
| 291 |
+
ref_rows = rows[mode_ref[rows]]
|
| 292 |
+
if ref_rows.size:
|
| 293 |
+
ui = u_ref[ref_rows, within[ref_rows]] # (R, 32)
|
| 294 |
+
out[ref_rows] = pack_q4_0(out[ref_rows], ui.astype(np.int16))
|
| 295 |
+
return out, improved, deq
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def pack_q3_scales(sc):
|
| 299 |
+
"""int subscales (B,16) in [-32,31] -> the 12 scale bytes per block."""
|
| 300 |
+
u = (np.asarray(sc, np.int32) + 32).astype(np.uint8) # 0..63
|
| 301 |
+
B = u.shape[0]
|
| 302 |
+
s12 = np.zeros((B, 12), np.uint8)
|
| 303 |
+
for k in range(4):
|
| 304 |
+
s12[:, k] = (u[:, k] & 15) | ((u[:, 8 + k] & 15) << 4)
|
| 305 |
+
s12[:, 4 + k] = (u[:, 4 + k] & 15) | ((u[:, 12 + k] & 15) << 4)
|
| 306 |
+
s12[:, 8 + k] = (((u[:, k] >> 4) & 3)
|
| 307 |
+
| (((u[:, 4 + k] >> 4) & 3) << 2)
|
| 308 |
+
| (((u[:, 8 + k] >> 4) & 3) << 4)
|
| 309 |
+
| (((u[:, 12 + k] >> 4) & 3) << 6))
|
| 310 |
+
return s12
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def assemble_q3_k(raw, codes, sc, d_bits):
|
| 314 |
+
"""rebuild Q3_K block bytes from given codes, sub-scale ints and fp16 d bits."""
|
| 315 |
+
out = pack_q3k(raw, codes)
|
| 316 |
+
out[:, 96:108] = pack_q3_scales(sc)
|
| 317 |
+
out[:, 108:110] = np.asarray(d_bits, "<u2").view(np.uint8).reshape(-1, 2)
|
| 318 |
+
return out
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def refit_q3_k(raw, x, dc_lam, rad):
|
| 322 |
+
"""(d, sc) re-solve on Q3_K blocks; codes optionally re-projected.
|
| 323 |
+
Returns (new_raw, improved_mask, deq). Never mutates the input."""
|
| 324 |
+
codes, s, o = unpack_q3k(raw)
|
| 325 |
+
B = raw.shape[0]
|
| 326 |
+
d16 = np.ascontiguousarray(raw[:, 108] | (raw[:, 109].astype(np.uint16) << 8))
|
| 327 |
+
deq0 = s * codes + o
|
| 328 |
+
cost0 = _block_cost(x - deq0, dc_lam)
|
| 329 |
+
|
| 330 |
+
x16 = x.astype(np.float64).reshape(B, 16, 16)
|
| 331 |
+
c16 = codes.astype(np.float64).reshape(B, 16, 16)
|
| 332 |
+
Sxc = np.sum(x16 * c16, axis=2) # (B,16)
|
| 333 |
+
Scc = np.sum(c16 * c16, axis=2)
|
| 334 |
+
csum = np.sum(c16, axis=2)
|
| 335 |
+
axg = np.sum(x16 * x16, axis=2)
|
| 336 |
+
xtot = x16.reshape(B, 256).sum(axis=1)
|
| 337 |
+
|
| 338 |
+
win_bits = _ulp_window(d16, rad) # (B, W)
|
| 339 |
+
# multiplicative ladder: joint d*sc rescales beyond the ULP window
|
| 340 |
+
d0 = _val16(d16)
|
| 341 |
+
lad_bits = _bits16(d0[:, None] * (2.0 ** np.arange(-8, 9)))
|
| 342 |
+
lad_bits = np.where(np.isfinite(_val16(lad_bits)) & (_val16(lad_bits) > 0),
|
| 343 |
+
lad_bits, d16[:, None])
|
| 344 |
+
d_ls = np.where(Scc.sum(1) > 1e-30,
|
| 345 |
+
Sxc.sum(1) / np.maximum(Scc.sum(1), 1e-30), 0.0)
|
| 346 |
+
b_ls = np.where((d_ls > 0) & (d_ls < 6e4), _bits16(d_ls), 0)
|
| 347 |
+
all_bits = np.concatenate([win_bits, lad_bits, b_ls[:, None]], axis=1) # (B, W+18)
|
| 348 |
+
dv = _val16(all_bits) # (B, W+18)
|
| 349 |
+
valid = (dv > 0) & np.isfinite(dv)
|
| 350 |
+
dv = np.where(valid, dv, np.inf)
|
| 351 |
+
W = all_bits.shape[1]
|
| 352 |
+
|
| 353 |
+
with np.errstate(invalid="ignore"):
|
| 354 |
+
sc_c = np.clip(np.rint(Sxc[:, None, :] /
|
| 355 |
+
np.maximum(dv[:, :, None] * Scc[:, None, :], 1e-30)),
|
| 356 |
+
-32, 31).astype(np.int64) # closed-form sc* per (block, cand)
|
| 357 |
+
ds = dv[:, :, None] * sc_c
|
| 358 |
+
sse_c = np.sum(axg[:, None, :] - 2.0 * ds * Sxc[:, None, :]
|
| 359 |
+
+ ds * ds * Scc[:, None, :], axis=2)
|
| 360 |
+
dc_c = xtot[:, None] - np.einsum("bdg,bg->bd", sc_c, csum) * dv
|
| 361 |
+
cost_keep = sse_c + dc_lam * dc_c * dc_c # codes frozen variant
|
| 362 |
+
|
| 363 |
+
# codes re-projected to the candidate frame (vec per group is (B,W+1,16))
|
| 364 |
+
safe_frame = np.maximum(np.abs(ds), 1e-30)
|
| 365 |
+
c_ref = np.clip(np.rint(x16[:, None, :, :] / safe_frame[:, :, :, None]), -4, 3)
|
| 366 |
+
deq_ref = dv[:, :, None, None] * sc_c[:, :, :, None] * c_ref # (B,W+1,16,16)
|
| 367 |
+
deq_ref = deq_ref.reshape(B, W, 256)
|
| 368 |
+
cost_ref = _block_cost(x[:, None, :] - deq_ref, dc_lam)
|
| 369 |
+
cost_keep = np.where(~valid, np.inf, cost_keep)
|
| 370 |
+
cost_ref = np.where(~valid, np.inf, cost_ref)
|
| 371 |
+
|
| 372 |
+
table_sc = np.concatenate([sc_c, sc_c], axis=1) # (B, 2W, 16)
|
| 373 |
+
table_bits = np.concatenate([all_bits, all_bits], axis=1)
|
| 374 |
+
table_cost = np.concatenate([cost_keep, cost_ref], axis=1)
|
| 375 |
+
pick = np.argmin(table_cost, axis=1)
|
| 376 |
+
cost_new = table_cost[np.arange(B), pick]
|
| 377 |
+
mode_ref = pick >= W
|
| 378 |
+
within = pick % W
|
| 379 |
+
improved = _better(cost_new, cost0)
|
| 380 |
+
rows = np.where(improved)[0]
|
| 381 |
+
if rows.size == 0:
|
| 382 |
+
return raw, improved, deq0
|
| 383 |
+
deq = deq0.copy()
|
| 384 |
+
out = raw.copy()
|
| 385 |
+
for i in rows:
|
| 386 |
+
p = pick[i]
|
| 387 |
+
b = table_bits[i, p]
|
| 388 |
+
sc_int = table_sc[i, p].astype(np.int32)
|
| 389 |
+
if mode_ref[i]:
|
| 390 |
+
cs = c_ref[i, within[i]].reshape(256).astype(np.int16)
|
| 391 |
+
deq[i] = deq_ref[i, within[i]]
|
| 392 |
+
else:
|
| 393 |
+
cs = codes[i].astype(np.int16)
|
| 394 |
+
deq[i] = (dv[i, within[i]] * sc_int.astype(np.float64)
|
| 395 |
+
.repeat(16, axis=0).reshape(256) * codes[i].astype(np.float64))
|
| 396 |
+
out[i] = assemble_q3_k(out[i][None, :], cs[None, :], sc_int[None, :],
|
| 397 |
+
np.array([b]))[0]
|
| 398 |
+
return out, improved, deq
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
def refit_q2_k(raw, x, dc_lam, rad):
|
| 402 |
+
"""(d, dmin) closed-form 2x2 LS re-solve on Q2_K blocks (codes and sub-scale
|
| 403 |
+
nibbles frozen); codes never change, so only the frame moves. Returns
|
| 404 |
+
(new_raw, improved_mask, deq). Never mutates the input."""
|
| 405 |
+
codes, s, o = unpack_q2k(raw)
|
| 406 |
+
B = raw.shape[0]
|
| 407 |
+
d16 = np.ascontiguousarray(raw[:, 80] | (raw[:, 81].astype(np.uint16) << 8))
|
| 408 |
+
m16 = np.ascontiguousarray(raw[:, 82] | (raw[:, 83].astype(np.uint16) << 8))
|
| 409 |
+
sc = raw[:, :16].astype(np.int32)
|
| 410 |
+
u_e = ((sc & 15).repeat(16, axis=1) * codes).astype(np.float64) # deq = d*u - m*v
|
| 411 |
+
v_e = np.repeat((sc >> 4), 16, axis=1).astype(np.float64) # v = lm't per group
|
| 412 |
+
deq0 = s * codes + o
|
| 413 |
+
cost0 = _block_cost(x - deq0, dc_lam)
|
| 414 |
+
|
| 415 |
+
Saa = np.sum(u_e * u_e, axis=1)
|
| 416 |
+
Sab = np.sum(u_e * v_e, axis=1)
|
| 417 |
+
Sbb = np.sum(v_e * v_e, axis=1)
|
| 418 |
+
Sxa = np.sum(x.astype(np.float64) * u_e, axis=1)
|
| 419 |
+
Sxb = np.sum(x.astype(np.float64) * v_e, axis=1)
|
| 420 |
+
det = Saa * Sbb - Sab * Sab
|
| 421 |
+
ok = (det > 1e-30) & np.isfinite(det)
|
| 422 |
+
safe = np.where(ok, det, 1.0)
|
| 423 |
+
d_star = (Sbb * Sxa - Sab * Sxb) / safe
|
| 424 |
+
m_star = (Sab * Sxa - Saa * Sxb) / safe
|
| 425 |
+
ok &= (d_star > 0) & (m_star > 0) & np.isfinite(d_star) & np.isfinite(m_star)
|
| 426 |
+
|
| 427 |
+
db_closed = np.where(ok, _bits16(np.where(ok, d_star, 0.0)), 0)
|
| 428 |
+
mb_closed = np.where(ok, _bits16(np.where(ok, m_star, 0.0)), 0)
|
| 429 |
+
# candidate families: (d bits column, dmin bits column, width)
|
| 430 |
+
# [0] closed-form pair, [1] d window (m frozen), [2] m window (d frozen)
|
| 431 |
+
fams = [
|
| 432 |
+
(db_closed[:, None], mb_closed[:, None], 1),
|
| 433 |
+
(_ulp_window(d16, rad), np.broadcast_to(m16[:, None], (B, 2 * rad + 1)), 2 * rad + 1),
|
| 434 |
+
(np.broadcast_to(d16[:, None], (B, 2 * rad + 1)), _ulp_window(m16, rad), 2 * rad + 1),
|
| 435 |
+
]
|
| 436 |
+
table_db = np.concatenate([f[0] for f in fams], axis=1)
|
| 437 |
+
table_mb = np.concatenate([f[1] for f in fams], axis=1)
|
| 438 |
+
offs = np.cumsum([0] + [f[2] for f in fams])
|
| 439 |
+
dv_t = _val16(np.ascontiguousarray(table_db))
|
| 440 |
+
mv_t = _val16(np.ascontiguousarray(table_mb))
|
| 441 |
+
bad = (dv_t <= 0) | (mv_t < 0) | ~np.isfinite(dv_t) | ~np.isfinite(mv_t)
|
| 442 |
+
dv_t = np.where(bad, np.inf, dv_t)
|
| 443 |
+
deq_t = dv_t[:, :, None] * u_e[:, None, :] - mv_t[:, :, None] * v_e[:, None, :]
|
| 444 |
+
cost_t = np.where(bad, np.inf, _block_cost(x[:, None, :] - deq_t, dc_lam))
|
| 445 |
+
pick = np.argmin(cost_t, axis=1)
|
| 446 |
+
improved = _better(cost_t[np.arange(B), pick], cost0)
|
| 447 |
+
rows = np.where(improved)[0]
|
| 448 |
+
if rows.size == 0:
|
| 449 |
+
return raw, improved, deq0
|
| 450 |
+
deq = deq0.copy()
|
| 451 |
+
out = raw.copy()
|
| 452 |
+
for i in rows:
|
| 453 |
+
p = pick[i]
|
| 454 |
+
dv_i = _val16(np.array([table_db[i, p]]))[0]
|
| 455 |
+
mv_i = _val16(np.array([table_mb[i, p]]))[0]
|
| 456 |
+
deq[i] = dv_i * u_e[i] - mv_i * v_e[i]
|
| 457 |
+
out[i, 80] = table_db[i, p] & 0xFF
|
| 458 |
+
out[i, 81] = (table_db[i, p] >> 8) & 0xFF
|
| 459 |
+
out[i, 82] = table_mb[i, p] & 0xFF
|
| 460 |
+
out[i, 83] = (table_mb[i, p] >> 8) & 0xFF
|
| 461 |
+
return out, improved, deq
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
REFIT = {QT.Q2_K: refit_q2_k, QT.Q3_K: refit_q3_k, QT.Q4_0: refit_q4_0}
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
def refit_batched(fn, raw, x, dc_lam, rad, batch):
|
| 468 |
+
"""run a re-fit in slices of `batch` blocks (peak memory ~ batch * candidates * 256 * 8 B)."""
|
| 469 |
+
outs, imps, deqs = [], [], []
|
| 470 |
+
for a in range(0, raw.shape[0], batch):
|
| 471 |
+
o, i, d = fn(raw[a : a + batch], x[a : a + batch], dc_lam, rad)
|
| 472 |
+
outs.append(o)
|
| 473 |
+
imps.append(i)
|
| 474 |
+
deqs.append(d)
|
| 475 |
+
return np.concatenate(outs), np.concatenate(imps), np.concatenate(deqs)
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
# ---------------------------------------------------------------------------
|
| 479 |
+
# source access
|
| 480 |
+
# ---------------------------------------------------------------------------
|
| 481 |
+
def source_elements(t, a, b):
|
| 482 |
+
"""float32 view of elements [a, b) of a source tensor (F32 / F16 / BF16)."""
|
| 483 |
+
data = np.asarray(t.data)
|
| 484 |
+
tt = t.tensor_type
|
| 485 |
+
if tt == QT.BF16:
|
| 486 |
+
flat = data.reshape(-1).view(np.uint16)[a:b].astype(np.uint32) << 16
|
| 487 |
+
return flat.view(np.float32)
|
| 488 |
+
flat = data.reshape(-1)[a:b]
|
| 489 |
+
if tt in (QT.F16, QT.F32):
|
| 490 |
+
return flat.astype(np.float32)
|
| 491 |
+
raise ValueError(f"source tensor type {tt.name} is not F32/F16/BF16")
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
def passthrough_chunks(t, st_t, chunk_elems):
|
| 495 |
+
"""Yield (source, quant) float32 flat chunks for a tensor we do not drag.
|
| 496 |
+
|
| 497 |
+
F32/F16/BF16 quant tensors are read directly; any other type gguf-py can
|
| 498 |
+
decode (Q8_0, Q6_K, ...) is dequantized, so its REAL displacement from the
|
| 499 |
+
source is measured instead of assumed to be zero. Raises ValueError /
|
| 500 |
+
NotImplementedError when a type cannot be decoded."""
|
| 501 |
+
ne = int(t.n_elements)
|
| 502 |
+
tt = t.tensor_type
|
| 503 |
+
if tt in (QT.F32, QT.F16, QT.BF16):
|
| 504 |
+
for a in range(0, ne, chunk_elems):
|
| 505 |
+
b = min(a + chunk_elems, ne)
|
| 506 |
+
yield source_elements(st_t, a, b), source_elements(t, a, b)
|
| 507 |
+
return
|
| 508 |
+
bs, ts = GGML_QUANT_SIZES[tt]
|
| 509 |
+
qdata = np.asarray(t.data).reshape(-1)
|
| 510 |
+
per = max(1, chunk_elems // bs)
|
| 511 |
+
for b0 in range(0, ne // bs, per):
|
| 512 |
+
b1 = min(b0 + per, ne // bs)
|
| 513 |
+
raw = np.array(qdata[b0 * ts : b1 * ts]).reshape(b1 - b0, ts)
|
| 514 |
+
yield (source_elements(st_t, b0 * bs, b1 * bs),
|
| 515 |
+
gq.dequantize(raw, tt).reshape(-1).astype(np.float32))
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
# ---------------------------------------------------------------------------
|
| 519 |
+
def parse_args(argv=None):
|
| 520 |
+
p = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
| 521 |
+
p.add_argument("--source", required=True, help="F32/F16/BF16 GGUF")
|
| 522 |
+
p.add_argument("--quant", required=True, help="quantized GGUF (Q2_K/Q3_K/Q4_0 tensors)")
|
| 523 |
+
p.add_argument("--output", default="", help="default: <quant>-repaired.gguf")
|
| 524 |
+
p.add_argument("--thresh", type=float, default=1.0,
|
| 525 |
+
help="outlier threshold in code steps (default 1.0)")
|
| 526 |
+
p.add_argument("--dry-run", action="store_true", help="report only, write nothing")
|
| 527 |
+
p.add_argument("--force", action="store_true", help="overwrite an existing --output")
|
| 528 |
+
p.add_argument("--fix-frames", action="store_true",
|
| 529 |
+
help="frame re-fit on blocks with frame-limited outliers "
|
| 530 |
+
"(guarded per block; only blocks that improve change)")
|
| 531 |
+
p.add_argument("--ulp-radius", type=int, default=8,
|
| 532 |
+
help="fp16 ULP window radius for the frame re-fit")
|
| 533 |
+
p.add_argument("--refit-batch", type=int, default=1024,
|
| 534 |
+
help="blocks per re-fit slice (memory control)")
|
| 535 |
+
p.add_argument("--dc-lambda", type=float, default=0.0,
|
| 536 |
+
help="DC^2 coupling in the frame re-fit cost")
|
| 537 |
+
p.add_argument("--tensor-substr", default="", help="comma-separated name filters")
|
| 538 |
+
p.add_argument("--catalog", default="", help="CSV of every moved weight")
|
| 539 |
+
p.add_argument("--dump-dir", default="", help="write full src/quant lattices + displacement maps (float16)")
|
| 540 |
+
p.add_argument("--chunk-elems", type=int, default=1 << 23)
|
| 541 |
+
return p.parse_args(argv)
|
| 542 |
+
|
| 543 |
+
|
| 544 |
+
def main(argv=None):
|
| 545 |
+
args = parse_args(argv)
|
| 546 |
+
src_path, q_path = Path(args.source), Path(args.quant)
|
| 547 |
+
out_path = Path(args.output) if args.output else q_path.with_name(q_path.stem + "-repaired.gguf")
|
| 548 |
+
|
| 549 |
+
src = GGUFReader(str(src_path))
|
| 550 |
+
qr = GGUFReader(str(q_path))
|
| 551 |
+
src_by_name = {t.name: t for t in src.tensors}
|
| 552 |
+
subs = [s for s in args.tensor_substr.split(",") if s]
|
| 553 |
+
|
| 554 |
+
# whole lattice: every tensor of the quant file, in file order
|
| 555 |
+
todo = [t for t in qr.tensors if not subs or any(s in t.name for s in subs)]
|
| 556 |
+
n_drag = sum(1 for t in todo if t.tensor_type in FORMATS)
|
| 557 |
+
n_pass = len(todo) - n_drag
|
| 558 |
+
print(f"whole lattice: {len(todo)} tensors ({n_drag} quantized to drag, "
|
| 559 |
+
f"{n_pass} passthrough)")
|
| 560 |
+
|
| 561 |
+
if not args.dry_run:
|
| 562 |
+
if out_path.exists() and not args.force:
|
| 563 |
+
sys.exit(f"{out_path} exists (use --force to overwrite)")
|
| 564 |
+
print(f"copying {q_path.name} -> {out_path.name} ...")
|
| 565 |
+
shutil.copyfile(q_path, out_path)
|
| 566 |
+
out_f = None if args.dry_run else open(out_path, "r+b")
|
| 567 |
+
|
| 568 |
+
cat = open(args.catalog, "w") if args.catalog else None
|
| 569 |
+
if cat:
|
| 570 |
+
cat.write("tensor,block,idx,fmt,source,old,new,old_code,new_code\n")
|
| 571 |
+
dump = Path(args.dump_dir) if args.dump_dir else None
|
| 572 |
+
if dump:
|
| 573 |
+
dump.mkdir(parents=True, exist_ok=True)
|
| 574 |
+
|
| 575 |
+
grand = None
|
| 576 |
+
pt_w, pt_sse = 0, 0.0
|
| 577 |
+
t0 = time.time()
|
| 578 |
+
for t in todo:
|
| 579 |
+
safe = t.name.replace("/", "__")
|
| 580 |
+
st_t = src_by_name.get(t.name)
|
| 581 |
+
if st_t is None:
|
| 582 |
+
print(f" skip {t.name}: not in source")
|
| 583 |
+
continue
|
| 584 |
+
if int(st_t.n_elements) != int(t.n_elements):
|
| 585 |
+
print(f" skip {t.name}: element count differs")
|
| 586 |
+
continue
|
| 587 |
+
|
| 588 |
+
if t.tensor_type not in FORMATS:
|
| 589 |
+
# not dragged, but still measured: the lattices only coincide for
|
| 590 |
+
# F32/F16/BF16 - other quant types (Q8_0, Q6_K...) are decoded.
|
| 591 |
+
fhs = []
|
| 592 |
+
if dump:
|
| 593 |
+
fhs = [open(dump / f"{safe}.{k}", "wb") for k in
|
| 594 |
+
("src.f16", "qnt.f16", "delta.before.f16", "delta.after.f16")]
|
| 595 |
+
n = sse = mx = 0
|
| 596 |
+
try:
|
| 597 |
+
for x, q in passthrough_chunks(t, st_t, args.chunk_elems):
|
| 598 |
+
d = x - q
|
| 599 |
+
n += d.size
|
| 600 |
+
sse += float(np.sum(d.astype(np.float64) ** 2))
|
| 601 |
+
mx = max(mx, float(np.max(np.abs(d))) if d.size else 0.0)
|
| 602 |
+
if fhs:
|
| 603 |
+
x.astype(np.float16).tofile(fhs[0])
|
| 604 |
+
q.astype(np.float16).tofile(fhs[1])
|
| 605 |
+
d.astype(np.float16).tofile(fhs[2])
|
| 606 |
+
d.astype(np.float16).tofile(fhs[3])
|
| 607 |
+
except (ValueError, NotImplementedError, KeyError) as e:
|
| 608 |
+
print(f" untouched {t.name} [{t.tensor_type.name}]: cannot measure ({e})")
|
| 609 |
+
else:
|
| 610 |
+
pt_w += n
|
| 611 |
+
pt_sse += sse
|
| 612 |
+
if mx > 0:
|
| 613 |
+
print(f" untouched {t.name} [{t.tensor_type.name}]: "
|
| 614 |
+
f"rmse {(sse / max(n, 1)) ** 0.5:.4e} max|d| {mx:.3e} (not modified)")
|
| 615 |
+
finally:
|
| 616 |
+
for fh in fhs:
|
| 617 |
+
fh.close()
|
| 618 |
+
continue
|
| 619 |
+
|
| 620 |
+
f = FORMATS[t.tensor_type]
|
| 621 |
+
be, bb = f["be"], f["bb"]
|
| 622 |
+
nblocks = int(t.n_elements) // be
|
| 623 |
+
qdata = np.asarray(t.data).reshape(-1)
|
| 624 |
+
blocks_per_chunk = max(1, args.chunk_elems // be)
|
| 625 |
+
tot = None
|
| 626 |
+
fs = fq = fd_b = fd_a = None
|
| 627 |
+
if dump:
|
| 628 |
+
fs = open(dump / f"{safe}.src.f16", "wb")
|
| 629 |
+
fq = open(dump / f"{safe}.qnt.f16", "wb")
|
| 630 |
+
fd_b = open(dump / f"{safe}.delta.before.f16", "wb")
|
| 631 |
+
fd_a = open(dump / f"{safe}.delta.after.f16", "wb")
|
| 632 |
+
for b0 in range(0, nblocks, blocks_per_chunk):
|
| 633 |
+
b1 = min(b0 + blocks_per_chunk, nblocks)
|
| 634 |
+
raw = np.array(qdata[b0 * bb : b1 * bb]).reshape(b1 - b0, bb)
|
| 635 |
+
x = source_elements(st_t, b0 * be, b1 * be).reshape(b1 - b0, be)
|
| 636 |
+
new_raw, st, move, flim, deq, deq2, c_old, c_new = drag_chunk(t.tensor_type, raw, x, args.thresh)
|
| 637 |
+
if args.fix_frames and flim.any():
|
| 638 |
+
sub_raw = new_raw[flim]
|
| 639 |
+
ftype = t.tensor_type
|
| 640 |
+
rr, improved, deq_r = refit_batched(REFIT[ftype], sub_raw, x[flim],
|
| 641 |
+
args.dc_lambda, args.ulp_radius,
|
| 642 |
+
args.refit_batch)
|
| 643 |
+
if improved.any():
|
| 644 |
+
new_raw = new_raw.copy()
|
| 645 |
+
new_raw[flim] = rr
|
| 646 |
+
e_fix = x[flim] - deq_r # only improved rows differ
|
| 647 |
+
deq2 = deq2.copy()
|
| 648 |
+
deq2[flim] = deq_r
|
| 649 |
+
# merge the re-fit stats into the chunk stats
|
| 650 |
+
e2f = x - deq2
|
| 651 |
+
st["sse_after"] = float(np.sum(e2f.astype(np.float64) ** 2))
|
| 652 |
+
st["max_after"] = float(np.max(np.abs(e2f)))
|
| 653 |
+
st["dc_after"] = float(np.sum(np.abs(e2f.sum(axis=1))))
|
| 654 |
+
st["blocks_frame_fixed"] = st.get("blocks_frame_fixed", 0) + int(improved.sum())
|
| 655 |
+
# catalogue every weight whose position changed (drag OR frame re-fit)
|
| 656 |
+
move = deq2 != deq
|
| 657 |
+
c_new = UNPACK[ftype](new_raw)[0]
|
| 658 |
+
if (st["moved"] or st.get("blocks_frame_fixed", 0)) and out_f is not None:
|
| 659 |
+
out_f.seek(int(t.data_offset) + b0 * bb)
|
| 660 |
+
out_f.write(new_raw.tobytes())
|
| 661 |
+
if cat and (st["moved"] or st.get("blocks_frame_fixed", 0)):
|
| 662 |
+
bi, ei = np.nonzero(move)
|
| 663 |
+
for i, j in zip(bi, ei):
|
| 664 |
+
cat.write(f"{t.name},{b0 + int(i)},{int(j)},{f['name']},"
|
| 665 |
+
f"{x[i, j]:.8g},{deq[i, j]:.8g},{deq2[i, j]:.8g},"
|
| 666 |
+
f"{int(c_old[i, j])},{int(c_new[i, j])}\n")
|
| 667 |
+
if dump:
|
| 668 |
+
x.astype(np.float16).tofile(fs)
|
| 669 |
+
deq2.astype(np.float16).tofile(fq)
|
| 670 |
+
(x - deq).astype(np.float16).tofile(fd_b)
|
| 671 |
+
(x - deq2).astype(np.float16).tofile(fd_a)
|
| 672 |
+
tot = stats_add(tot, st)
|
| 673 |
+
for fh in (fs, fq, fd_b, fd_a):
|
| 674 |
+
if fh:
|
| 675 |
+
fh.close()
|
| 676 |
+
if tot is None:
|
| 677 |
+
continue
|
| 678 |
+
rms0 = (tot["sse_before"] / tot["weights"]) ** 0.5
|
| 679 |
+
rms1 = (tot["sse_after"] / tot["weights"]) ** 0.5
|
| 680 |
+
print(f" {t.name} [{f['name']}] outliers {tot['outliers']}/{tot['weights']} "
|
| 681 |
+
f"moved {tot['moved']} (blocks {tot['blocks_changed']}/{tot['blocks']}) "
|
| 682 |
+
f"frame-limited {tot['frame_limited']} in {tot.get('blocks_frame_limited', 0)} blocks"
|
| 683 |
+
+ (f", frames fixed {tot.get('blocks_frame_fixed', 0)}"
|
| 684 |
+
if tot.get('blocks_frame_fixed', 0) else "")
|
| 685 |
+
+ f" | rmse {rms0:.4e}->{rms1:.4e} "
|
| 686 |
+
f"max|e| {tot['max_before']:.3e}->{tot['max_after']:.3e} "
|
| 687 |
+
f"mean|DC| {tot['dc_before']/tot['blocks']:.3e}->{tot['dc_after']/tot['blocks']:.3e}")
|
| 688 |
+
grand = stats_add(grand, tot)
|
| 689 |
+
|
| 690 |
+
if cat:
|
| 691 |
+
cat.close()
|
| 692 |
+
if out_f:
|
| 693 |
+
out_f.close()
|
| 694 |
+
if grand:
|
| 695 |
+
print(f"\nDRAGGED: {grand['weights']} weights | "
|
| 696 |
+
f"{grand['outliers']} outliers, {grand['moved']} moved, "
|
| 697 |
+
f"{grand['frame_limited']} frame-limited"
|
| 698 |
+
+ (f", {grand.get('blocks_frame_fixed', 0)} blocks frame-fixed"
|
| 699 |
+
if grand.get('blocks_frame_fixed', 0) else "")
|
| 700 |
+
+ f" | rmse {(grand['sse_before']/grand['weights'])**0.5:.4e} -> "
|
| 701 |
+
f"{(grand['sse_after']/grand['weights'])**0.5:.4e} "
|
| 702 |
+
f"max|d| {grand['max_before']:.3e} -> {grand['max_after']:.3e} "
|
| 703 |
+
f"({time.time()-t0:.0f}s)")
|
| 704 |
+
if pt_w:
|
| 705 |
+
print(f"UNTOUCHED: {pt_w} weights in non-dragged tensors, "
|
| 706 |
+
f"rmse vs source {(pt_sse/pt_w)**0.5:.4e}")
|
| 707 |
+
print("dry run: nothing written." if args.dry_run else f"done -> {out_path}")
|
| 708 |
+
|
| 709 |
+
|
| 710 |
+
if __name__ == "__main__":
|
| 711 |
+
main()
|