CompressedGemma commited on
Commit
8cd21f4
·
verified ·
1 Parent(s): 49c0661

Upload lattice_repair.py

Browse files
Files changed (1) hide show
  1. 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()