CompressedGemma commited on
Commit
2e3e81d
·
verified ·
1 Parent(s): dfb9420

Delete lattice_repair.py

Browse files
Files changed (1) hide show
  1. lattice_repair.py +0 -711
lattice_repair.py DELETED
@@ -1,711 +0,0 @@
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()