CompressedGemma commited on
Commit
c3e59d2
Β·
verified Β·
1 Parent(s): 4654109

--analog-imatrix: Reduce error in output by 15%

Browse files
Files changed (2) hide show
  1. hexstate_quantize.c +0 -0
  2. hexstate_requantize.py +204 -7
hexstate_quantize.c CHANGED
The diff for this file is too large to render. See raw diff
 
hexstate_requantize.py CHANGED
@@ -10,6 +10,14 @@ This bypasses the tokenizer parsing problem entirely β€” the source GGUF
10
 
11
  Usage:
12
  python3 hexstate_requantize.py input.gguf output.gguf
 
 
 
 
 
 
 
 
13
  """
14
 
15
  import struct
@@ -53,6 +61,22 @@ def _load_hexstate_lib():
53
  ctypes.c_int, # verbose
54
  ]
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  lib.hexstate_q2k_block_bytes.restype = ctypes.c_int
57
  lib.hexstate_q2k_block_bytes.argtypes = []
58
  lib.hexstate_q2k_block_elements.restype = ctypes.c_int
@@ -124,6 +148,121 @@ def _skip_gguf_kv_value(f, vtype):
124
  f.read(sz)
125
 
126
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  def read_imatrix(path):
128
  """Read llama.cpp importance matrix file (GGUF or legacy .dat format).
129
 
@@ -734,6 +873,27 @@ def main():
734
  print(f" WARNING: imatrix file not found: {imat_path}")
735
  break
736
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
737
  # Check for HPC C library
738
  use_hpc = _load_hexstate_lib() is not None
739
 
@@ -1057,7 +1217,14 @@ def main():
1057
  total_quant_bytes = 0
1058
  total_keep_bytes = 0
1059
  total_rmse = 0.0
1060
- q2k_rmse_sum = 0.0
 
 
 
 
 
 
 
1061
  q2k_tensor_count = 0
1062
 
1063
  for i, ti in enumerate(tensor_infos):
@@ -1228,7 +1395,29 @@ def main():
1228
  imat_full = np.tile(iw, n_rows)[:ti['n_elements']]
1229
 
1230
  n_el = ti['n_elements']
1231
- if use_hpc and n_el <= HPC_CHUNK:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1232
  # Small tensor β€” single HPC pass
1233
  q2k_data, n_blocks = quantize_tensor_q2k_hpc(f32, opt_mode=2, importance=imat_full)
1234
  elif use_hpc:
@@ -1274,8 +1463,8 @@ def main():
1274
  # ── Compute and report exact per-tensor RMSE ──
1275
  try:
1276
  CHUNK_BLK = 100_000 # blocks per chunk to bound memory
1277
- total_se = 0.0
1278
- total_n = 0
1279
  for ci in range(0, n_blocks, CHUNK_BLK):
1280
  ce = min(ci + CHUNK_BLK, n_blocks)
1281
  chunk_q = q2k_data[ci*84:ce*84]
@@ -1283,9 +1472,11 @@ def main():
1283
  orig_chunk = f32[ci*QK_K:ce*QK_K]
1284
  n_valid = min(len(orig_chunk), len(deq_chunk))
1285
  diff = orig_chunk[:n_valid] - deq_chunk[:n_valid]
1286
- total_se += np.sum(diff ** 2)
1287
  total_n += n_valid
1288
  tensor_rmse = np.sqrt(total_se / max(total_n, 1))
 
 
1289
  q2k_rmse_sum += tensor_rmse
1290
  q2k_tensor_count += 1
1291
  print(f"\n [Q2_K] {ti['name'][:55]} RMSE={tensor_rmse:.6e}")
@@ -1326,7 +1517,13 @@ def main():
1326
  print(f" β•‘ Compression: {compression:>42.1f}x β•‘")
1327
  if q2k_tensor_count > 0:
1328
  mean_rmse = q2k_rmse_sum / q2k_tensor_count
1329
- print(f" β•‘ Mean Q2_K RMSE: {mean_rmse:>12.6e} β•‘")
 
 
 
 
 
 
1330
  print(f" β•‘ Total time: {elapsed:>39.1f} sec β•‘")
1331
  print(" β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•")
1332
  print()
@@ -1335,4 +1532,4 @@ def main():
1335
 
1336
 
1337
  if __name__ == '__main__':
1338
- main()
 
10
 
11
  Usage:
12
  python3 hexstate_requantize.py input.gguf output.gguf
13
+
14
+ ANALOG mode (minimises LAYER OUTPUT error instead of weight RMSE):
15
+ --hessian FILE.hxh full X^T X per tensor (see write_hessian)
16
+ --analog-imatrix diagonal Hessian from --imatrix (weaker, no new
17
+ calibration needed)
18
+ --analog-sweeps N coordinate-descent sweeps (default 8)
19
+
20
+ In ANALOG mode weight RMSE RISES by design. Evaluate with perplexity.
21
  """
22
 
23
  import struct
 
61
  ctypes.c_int, # verbose
62
  ]
63
 
64
+ # ANALOG Q2_K (Hessian / output-error objective)
65
+ if hasattr(lib, 'hexstate_quantize_tensor_q2k_analog'):
66
+ lib.hexstate_quantize_tensor_q2k_analog.restype = None
67
+ lib.hexstate_quantize_tensor_q2k_analog.argtypes = [
68
+ ctypes.POINTER(ctypes.c_float), # weights
69
+ ctypes.c_int64, # n_elements
70
+ ctypes.c_int64, # n_in (row length)
71
+ ctypes.POINTER(ctypes.c_float), # H (n_in*n_in) or NULL
72
+ ctypes.c_void_p, # output
73
+ ctypes.POINTER(ctypes.c_float), # out_error
74
+ ctypes.c_int, # n_sweeps
75
+ ctypes.c_int, # verbose
76
+ ]
77
+ lib.hexstate_q2k_hessian_bytes.restype = ctypes.c_int64
78
+ lib.hexstate_q2k_hessian_bytes.argtypes = [ctypes.c_int64]
79
+
80
  lib.hexstate_q2k_block_bytes.restype = ctypes.c_int
81
  lib.hexstate_q2k_block_bytes.argtypes = []
82
  lib.hexstate_q2k_block_elements.restype = ctypes.c_int
 
148
  f.read(sz)
149
 
150
 
151
+ # ─── Hessian (X^T X) support for the ANALOG objective ───────────────────────
152
+ #
153
+ # FILE FORMAT (.hxh) β€” little-endian throughout
154
+ #
155
+ # magic 4 bytes b"HXH1"
156
+ # n_entries uint32
157
+ # repeated n_entries times:
158
+ # name_len uint32
159
+ # name name_len bytes, UTF-8 tensor name (GGUF naming)
160
+ # n_in uint32 row length; must be a multiple of 256
161
+ # H n_in*n_in float32, ROW-MAJOR
162
+ #
163
+ # H must be X^T X / n_samples for that layer's INPUT activations, damped on
164
+ # the diagonal (add ~1% of mean(diag) to keep it invertible). This is NOT a
165
+ # llama.cpp imatrix: an imatrix stores only diag(X^T X), the n_in diagonal
166
+ # entries. Entries may be shared between tensors with the same input space
167
+ # (e.g. attn_q / attn_k / attn_v) by writing the same H under each name.
168
+ #
169
+ # Memory: n_in^2 * 4 bytes. 4 MiB at n_in=1024, 64 MiB at n_in=4096.
170
+
171
+ HESSIAN_MAGIC = b"HXH1"
172
+
173
+
174
+ def read_hessian(path):
175
+ """Read a .hxh file. Returns dict: tensor_name -> float32 (n_in, n_in)."""
176
+ import struct as st
177
+ out = {}
178
+ with open(path, 'rb') as f:
179
+ if f.read(4) != HESSIAN_MAGIC:
180
+ raise ValueError(f"{path}: not a HXH1 Hessian file")
181
+ n_entries = st.unpack('<I', f.read(4))[0]
182
+ for _ in range(n_entries):
183
+ nl = st.unpack('<I', f.read(4))[0]
184
+ name = f.read(nl).decode('utf-8', errors='replace')
185
+ n_in = st.unpack('<I', f.read(4))[0]
186
+ buf = f.read(n_in * n_in * 4)
187
+ if len(buf) != n_in * n_in * 4:
188
+ raise ValueError(f"{path}: truncated entry for {name}")
189
+ out[name] = np.frombuffer(buf, dtype=np.float32).reshape(n_in, n_in)
190
+ return out
191
+
192
+
193
+ def write_hessian(path, entries):
194
+ """entries: dict name -> (n_in, n_in) array convertible to float32."""
195
+ import struct as st
196
+ with open(path, 'wb') as f:
197
+ f.write(HESSIAN_MAGIC)
198
+ f.write(st.pack('<I', len(entries)))
199
+ for name, H in entries.items():
200
+ H = np.ascontiguousarray(H, dtype=np.float32)
201
+ assert H.ndim == 2 and H.shape[0] == H.shape[1], name
202
+ nb = name.encode('utf-8')
203
+ f.write(st.pack('<I', len(nb)))
204
+ f.write(nb)
205
+ f.write(st.pack('<I', H.shape[0]))
206
+ f.write(H.tobytes())
207
+
208
+
209
+ def hessian_from_imatrix(imat_row, n_in, damp=0.01):
210
+ """Build a DIAGONAL Hessian from an existing imatrix row.
211
+
212
+ An imatrix gives diag(X^T X) only, so this discards all cross-column
213
+ correlation. It is strictly weaker than a real Hessian -- measured on a
214
+ synthetic layer, diagonal-only recovers roughly half the output-error
215
+ gain that a full H delivers -- but it needs no new calibration run and
216
+ lets the analog path be exercised with data you already have.
217
+ """
218
+ d = np.asarray(imat_row, dtype=np.float64).ravel()
219
+ if d.size < n_in:
220
+ d = np.tile(d, int(np.ceil(n_in / max(d.size, 1))))
221
+ d = d[:n_in].copy()
222
+ d[~np.isfinite(d)] = 0.0
223
+ if d.max() <= 0:
224
+ d[:] = 1.0
225
+ d += damp * d.mean()
226
+ H = np.zeros((n_in, n_in), dtype=np.float32)
227
+ np.fill_diagonal(H, d.astype(np.float32))
228
+ return H
229
+
230
+
231
+ def quantize_tensor_q2k_analog(f32_data, n_in, H, sweeps=0, verbose=1):
232
+ """Assign fresh 2-bit codes minimising LAYER OUTPUT error (not weight RMSE).
233
+
234
+ Weight RMSE is EXPECTED to rise; judge this path by perplexity.
235
+ Returns (bytes, n_blocks). Raises if the C library lacks the entry point.
236
+ """
237
+ lib = _load_hexstate_lib()
238
+ if lib is None or not hasattr(lib, 'hexstate_quantize_tensor_q2k_analog'):
239
+ raise RuntimeError("libhexstate_q2k.so has no analog entry point; rebuild")
240
+
241
+ n_elements = len(f32_data)
242
+ if n_elements % QK_K != 0:
243
+ pad = QK_K - (n_elements % QK_K)
244
+ f32_data = np.concatenate([f32_data, np.zeros(pad, dtype=np.float32)])
245
+ n_elements = len(f32_data)
246
+
247
+ n_blocks = n_elements // QK_K
248
+ output = np.zeros(n_blocks * lib.hexstate_q2k_block_bytes(), dtype=np.uint8)
249
+ error = ctypes.c_float(0.0)
250
+
251
+ f32c = np.ascontiguousarray(f32_data, dtype=np.float32)
252
+ Hc = np.ascontiguousarray(H, dtype=np.float32)
253
+ lib.hexstate_quantize_tensor_q2k_analog(
254
+ f32c.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
255
+ ctypes.c_int64(n_elements),
256
+ ctypes.c_int64(int(n_in)),
257
+ Hc.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
258
+ output.ctypes.data_as(ctypes.c_void_p),
259
+ ctypes.byref(error),
260
+ ctypes.c_int(int(sweeps)),
261
+ ctypes.c_int(int(verbose)),
262
+ )
263
+ return output.tobytes(), n_blocks
264
+
265
+
266
  def read_imatrix(path):
267
  """Read llama.cpp importance matrix file (GGUF or legacy .dat format).
268
 
 
873
  print(f" WARNING: imatrix file not found: {imat_path}")
874
  break
875
 
876
+ # ── ANALOG objective options ──
877
+ hessian_data = None
878
+ analog_from_imat = '--analog-imatrix' in sys.argv
879
+ analog_sweeps = 0
880
+ for i, arg in enumerate(sys.argv):
881
+ if arg == '--hessian' and i + 1 < len(sys.argv):
882
+ hpath = sys.argv[i + 1]
883
+ if os.path.exists(hpath):
884
+ hessian_data = read_hessian(hpath)
885
+ print(f" Loaded Hessian: {len(hessian_data)} tensors from {hpath}")
886
+ else:
887
+ print(f" WARNING: Hessian file not found: {hpath}")
888
+ elif arg == '--analog-sweeps' and i + 1 < len(sys.argv):
889
+ analog_sweeps = int(sys.argv[i + 1])
890
+ use_analog = (hessian_data is not None) or analog_from_imat
891
+ if use_analog:
892
+ print(" ANALOG MODE: minimising LAYER OUTPUT error, not weight RMSE.")
893
+ print(" Weight RMSE will RISE. Evaluate with perplexity, not RMSE.")
894
+ if analog_from_imat and hessian_data is None:
895
+ print(" Using DIAGONAL Hessian from imatrix (~half the gain of full H).")
896
+
897
  # Check for HPC C library
898
  use_hpc = _load_hexstate_lib() is not None
899
 
 
1217
  total_quant_bytes = 0
1218
  total_keep_bytes = 0
1219
  total_rmse = 0.0
1220
+ # Element-weighted global accumulators. A plain mean of per-tensor
1221
+ # RMSEs weights a 4K-element norm tensor the same as a 100M-element
1222
+ # FFN, so it is dominated by the small tensors and is the wrong
1223
+ # thing to tune against. Track sum-of-squared-error and element
1224
+ # count instead, and derive the true global RMSE at the end.
1225
+ q2k_se_sum = 0.0 # Ξ£ squared error over all Q2_K elements
1226
+ q2k_elem_count = 0 # Ξ£ elements
1227
+ q2k_rmse_sum = 0.0 # kept for the unweighted mean (reported too)
1228
  q2k_tensor_count = 0
1229
 
1230
  for i, ti in enumerate(tensor_infos):
 
1395
  imat_full = np.tile(iw, n_rows)[:ti['n_elements']]
1396
 
1397
  n_el = ti['n_elements']
1398
+
1399
+ # ── ANALOG path: fresh 2-bit codes vs output error ──
1400
+ analog_H = None
1401
+ analog_n_in = ti['dims'][0] if ti.get('dims') else 0
1402
+ if use_analog and analog_n_in and analog_n_in % QK_K == 0 \
1403
+ and n_el % analog_n_in == 0:
1404
+ if hessian_data and ti['name'] in hessian_data:
1405
+ Hcand = hessian_data[ti['name']]
1406
+ if Hcand.shape[0] == analog_n_in:
1407
+ analog_H = Hcand
1408
+ if analog_H is None and analog_from_imat and imatrix_data \
1409
+ and ti['name'] in imatrix_data:
1410
+ analog_H = hessian_from_imatrix(
1411
+ imatrix_data[ti['name']], analog_n_in)
1412
+
1413
+ if analog_H is not None:
1414
+ q2k_data, n_blocks = quantize_tensor_q2k_analog(
1415
+ f32, analog_n_in, analog_H, sweeps=analog_sweeps)
1416
+ src = 'full-H' if (hessian_data and ti['name'] in hessian_data) \
1417
+ else 'diag-H'
1418
+ print(f" [analog:{src}] n_in={analog_n_in} "
1419
+ f"rows={n_el // analog_n_in}")
1420
+ elif use_hpc and n_el <= HPC_CHUNK:
1421
  # Small tensor β€” single HPC pass
1422
  q2k_data, n_blocks = quantize_tensor_q2k_hpc(f32, opt_mode=2, importance=imat_full)
1423
  elif use_hpc:
 
1463
  # ── Compute and report exact per-tensor RMSE ──
1464
  try:
1465
  CHUNK_BLK = 100_000 # blocks per chunk to bound memory
1466
+ total_se = 0.0 # float64: np.sum on float32 would
1467
+ total_n = 0 # round away small terms on big tensors
1468
  for ci in range(0, n_blocks, CHUNK_BLK):
1469
  ce = min(ci + CHUNK_BLK, n_blocks)
1470
  chunk_q = q2k_data[ci*84:ce*84]
 
1472
  orig_chunk = f32[ci*QK_K:ce*QK_K]
1473
  n_valid = min(len(orig_chunk), len(deq_chunk))
1474
  diff = orig_chunk[:n_valid] - deq_chunk[:n_valid]
1475
+ total_se += float(np.sum(diff.astype(np.float64) ** 2))
1476
  total_n += n_valid
1477
  tensor_rmse = np.sqrt(total_se / max(total_n, 1))
1478
+ q2k_se_sum += total_se
1479
+ q2k_elem_count += total_n
1480
  q2k_rmse_sum += tensor_rmse
1481
  q2k_tensor_count += 1
1482
  print(f"\n [Q2_K] {ti['name'][:55]} RMSE={tensor_rmse:.6e}")
 
1517
  print(f" β•‘ Compression: {compression:>42.1f}x β•‘")
1518
  if q2k_tensor_count > 0:
1519
  mean_rmse = q2k_rmse_sum / q2k_tensor_count
1520
+ # Global RMSE = sqrt(Ξ£ SE / Ξ£ N): every quantized weight counts once,
1521
+ # regardless of which tensor it lives in. This is the number to tune
1522
+ # against; the unweighted per-tensor mean is kept below for continuity
1523
+ # with previous runs but is dominated by small tensors.
1524
+ global_rmse = np.sqrt(q2k_se_sum / max(q2k_elem_count, 1))
1525
+ print(f" β•‘ Global Q2_K RMSE (element-wtd): {global_rmse:>12.6e} β•‘")
1526
+ print(f" β•‘ Mean Q2_K RMSE (per-tensor, unwtd): {mean_rmse:>12.6e} β•‘")
1527
  print(f" β•‘ Total time: {elapsed:>39.1f} sec β•‘")
1528
  print(" β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•")
1529
  print()
 
1532
 
1533
 
1534
  if __name__ == '__main__':
1535
+ main()