CompressedGemma commited on
Commit
e3887bb
·
verified ·
1 Parent(s): 4de4d5a

Upload 12 files

Browse files
Files changed (3) hide show
  1. __main__.py +33 -1
  2. gguf.py +176 -4
  3. q2k.py +14 -0
__main__.py CHANGED
@@ -14,6 +14,7 @@ Examples::
14
  from __future__ import annotations
15
 
16
  import argparse
 
17
 
18
  import numpy as np
19
 
@@ -25,6 +26,7 @@ from .gguf import (
25
  is_q2k_eligible,
26
  lookup_imatrix,
27
  read_imatrix,
 
28
  )
29
  from .pipeline import Q2KQuantizer
30
  from .reference import reference_solve, synthetic_cases
@@ -84,6 +86,29 @@ def _run_gguf(args, cfg: HPCConfig) -> None:
84
  return
85
 
86
  quantizer = Q2KQuantizer(cfg)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  if args.all_q2k:
88
  payload: dict[str, np.ndarray] = {}
89
  done = 0
@@ -191,7 +216,7 @@ def main() -> None:
191
  parser.add_argument("--importance", help="optional .npy importance array")
192
  parser.add_argument("--imatrix-gguf", help="llama.cpp iMatrix .gguf file")
193
  parser.add_argument("--imatrix", help="alias for --imatrix-gguf (GGUF or legacy .dat)")
194
- parser.add_argument("--output", help="output .npz")
195
  parser.add_argument("--demo", action="store_true", help="quantize synthetic weights")
196
  parser.add_argument("--blocks", type=int, default=64, help="demo block count")
197
  parser.add_argument("--std", type=float, default=0.02, help="demo weight std")
@@ -212,6 +237,13 @@ def main() -> None:
212
  return
213
 
214
  if args.gguf:
 
 
 
 
 
 
 
215
  _run_gguf(args, cfg)
216
  return
217
 
 
14
  from __future__ import annotations
15
 
16
  import argparse
17
+ from pathlib import Path
18
 
19
  import numpy as np
20
 
 
26
  is_q2k_eligible,
27
  lookup_imatrix,
28
  read_imatrix,
29
+ write_quantized_gguf,
30
  )
31
  from .pipeline import Q2KQuantizer
32
  from .reference import reference_solve, synthetic_cases
 
86
  return
87
 
88
  quantizer = Q2KQuantizer(cfg)
89
+ wants_gguf = bool(args.output and str(args.output).lower().endswith(".gguf"))
90
+ if wants_gguf:
91
+ if not args.all_q2k and not args.tensor:
92
+ args.all_q2k = True
93
+ if args.rows:
94
+ raise SystemExit("--rows is only valid for .npz sampling output")
95
+ names = {args.tensor} if args.tensor else None
96
+ stats = write_quantized_gguf(
97
+ args.gguf,
98
+ args.output,
99
+ quantizer,
100
+ imat=imat,
101
+ quantize_all=args.all_q2k,
102
+ tensor_names=names,
103
+ limit=args.limit,
104
+ verbose=True,
105
+ )
106
+ print(
107
+ f"wrote {args.output}: {int(stats['quantized'])}/{int(stats['tensors'])} tensors Q2_K, "
108
+ f"rmse={stats['rmse']:.6e}"
109
+ )
110
+ return
111
+
112
  if args.all_q2k:
113
  payload: dict[str, np.ndarray] = {}
114
  done = 0
 
216
  parser.add_argument("--importance", help="optional .npy importance array")
217
  parser.add_argument("--imatrix-gguf", help="llama.cpp iMatrix .gguf file")
218
  parser.add_argument("--imatrix", help="alias for --imatrix-gguf (GGUF or legacy .dat)")
219
+ parser.add_argument("--output", help="output .gguf (full model) or .npz (block arrays)")
220
  parser.add_argument("--demo", action="store_true", help="quantize synthetic weights")
221
  parser.add_argument("--blocks", type=int, default=64, help="demo block count")
222
  parser.add_argument("--std", type=float, default=0.02, help="demo weight std")
 
237
  return
238
 
239
  if args.gguf:
240
+ if (
241
+ not args.output
242
+ and not args.list_tensors
243
+ and (args.all_q2k or args.tensor)
244
+ ):
245
+ stem = Path(args.gguf).stem
246
+ args.output = str(Path(args.gguf).with_name(f"{stem}-Q2K.gguf"))
247
  _run_gguf(args, cfg)
248
  return
249
 
gguf.py CHANGED
@@ -20,7 +20,7 @@ from pathlib import Path
20
 
21
  import numpy as np
22
 
23
- from .q2k import QK_K, dequantize_params_batch, unpack_codes
24
 
25
  GGUF_MAGIC = 0x46554747
26
  DEFAULT_ALIGNMENT = 32
@@ -69,6 +69,14 @@ BLOCK_BYTES = {
69
  GGML_TYPE_Q8_0: 34,
70
  GGML_TYPE_Q2_K: 84,
71
  }
 
 
 
 
 
 
 
 
72
 
73
 
74
  @dataclass(slots=True)
@@ -154,17 +162,26 @@ class GGUFReader:
154
  def __init__(self, path: str | Path):
155
  self.path = Path(path)
156
  self.metadata: dict[str, object] = {}
 
157
  self.tensors: list[TensorInfo] = []
158
  with self.path.open("rb") as f:
159
  magic, version = struct.unpack("<II", f.read(8))
160
  if magic != GGUF_MAGIC:
161
  raise ValueError(f"{self.path} is not a GGUF file")
162
  self.version = version
 
163
  n_tensors, n_kv = struct.unpack("<QQ", f.read(16))
164
  for _ in range(n_kv):
165
  key = _read_string(f)
166
  vtype = struct.unpack("<I", f.read(4))[0]
 
167
  self.metadata[key] = _read_value(f, vtype)
 
 
 
 
 
 
168
  for _ in range(n_tensors):
169
  name = _read_string(f)
170
  n_dims = struct.unpack("<I", f.read(4))[0]
@@ -172,7 +189,9 @@ class GGUFReader:
172
  ttype = struct.unpack("<I", f.read(4))[0]
173
  offset = struct.unpack("<Q", f.read(8))[0]
174
  self.tensors.append(TensorInfo(name, dims, ttype, offset))
175
- self.data_start = _align(f.tell(), int(self.metadata.get("general.alignment", DEFAULT_ALIGNMENT)))
 
 
176
  self._by_name = {ti.name: ti for ti in self.tensors}
177
 
178
  def info(self, name: str) -> TensorInfo:
@@ -184,14 +203,30 @@ class GGUFReader:
184
  def list_tensors(self) -> list[TensorInfo]:
185
  return list(self.tensors)
186
 
187
- def read_tensor(self, name: str) -> tuple[np.ndarray, int]:
188
  ti = self.info(name)
189
  with self.path.open("rb") as f:
190
  f.seek(self.data_start + ti.offset)
191
  raw = f.read(ti.n_bytes)
192
  if len(raw) != ti.n_bytes:
193
  raise ValueError(f"{name}: truncated tensor data")
194
- return _dequantize(raw, ti), ti.row_width
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
 
196
 
197
  def _dequantize(raw: bytes, ti: TensorInfo) -> np.ndarray:
@@ -324,3 +359,140 @@ def is_q2k_eligible(name: str, dims: tuple[int, ...]) -> bool:
324
  for d in dims:
325
  n_elements *= int(d)
326
  return n_elements >= QK_K and n_elements % QK_K == 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  import numpy as np
22
 
23
+ from .q2k import QK_K, blocks_to_bytes, dequantize_params_batch, unpack_codes
24
 
25
  GGUF_MAGIC = 0x46554747
26
  DEFAULT_ALIGNMENT = 32
 
69
  GGML_TYPE_Q8_0: 34,
70
  GGML_TYPE_Q2_K: 84,
71
  }
72
+ SUPPORTED_READ_TYPES = {
73
+ GGML_TYPE_F32,
74
+ GGML_TYPE_F16,
75
+ GGML_TYPE_BF16,
76
+ GGML_TYPE_Q4_0,
77
+ GGML_TYPE_Q8_0,
78
+ GGML_TYPE_Q2_K,
79
+ }
80
 
81
 
82
  @dataclass(slots=True)
 
162
  def __init__(self, path: str | Path):
163
  self.path = Path(path)
164
  self.metadata: dict[str, object] = {}
165
+ self.kv_pairs: list[tuple[str, int, bytes]] = []
166
  self.tensors: list[TensorInfo] = []
167
  with self.path.open("rb") as f:
168
  magic, version = struct.unpack("<II", f.read(8))
169
  if magic != GGUF_MAGIC:
170
  raise ValueError(f"{self.path} is not a GGUF file")
171
  self.version = version
172
+ self.kv_start = f.tell()
173
  n_tensors, n_kv = struct.unpack("<QQ", f.read(16))
174
  for _ in range(n_kv):
175
  key = _read_string(f)
176
  vtype = struct.unpack("<I", f.read(4))[0]
177
+ value_start = f.tell()
178
  self.metadata[key] = _read_value(f, vtype)
179
+ value_end = f.tell()
180
+ f.seek(value_start)
181
+ raw = f.read(value_end - value_start)
182
+ f.seek(value_end)
183
+ self.kv_pairs.append((key, vtype, raw))
184
+ self.kv_end = f.tell()
185
  for _ in range(n_tensors):
186
  name = _read_string(f)
187
  n_dims = struct.unpack("<I", f.read(4))[0]
 
189
  ttype = struct.unpack("<I", f.read(4))[0]
190
  offset = struct.unpack("<Q", f.read(8))[0]
191
  self.tensors.append(TensorInfo(name, dims, ttype, offset))
192
+ self.info_end = f.tell()
193
+ self.alignment = int(self.metadata.get("general.alignment", DEFAULT_ALIGNMENT))
194
+ self.data_start = _align(self.info_end, self.alignment)
195
  self._by_name = {ti.name: ti for ti in self.tensors}
196
 
197
  def info(self, name: str) -> TensorInfo:
 
203
  def list_tensors(self) -> list[TensorInfo]:
204
  return list(self.tensors)
205
 
206
+ def read_tensor_raw(self, name: str) -> bytes:
207
  ti = self.info(name)
208
  with self.path.open("rb") as f:
209
  f.seek(self.data_start + ti.offset)
210
  raw = f.read(ti.n_bytes)
211
  if len(raw) != ti.n_bytes:
212
  raise ValueError(f"{name}: truncated tensor data")
213
+ return raw
214
+
215
+ def copy_tensor_raw_to(self, name: str, out, chunk: int = 1 << 20) -> None:
216
+ ti = self.info(name)
217
+ remaining = ti.n_bytes
218
+ with self.path.open("rb") as f:
219
+ f.seek(self.data_start + ti.offset)
220
+ while remaining > 0:
221
+ data = f.read(min(chunk, remaining))
222
+ if not data:
223
+ raise ValueError(f"{name}: truncated tensor data")
224
+ out.write(data)
225
+ remaining -= len(data)
226
+
227
+ def read_tensor(self, name: str) -> tuple[np.ndarray, int]:
228
+ raw = self.read_tensor_raw(name)
229
+ return _dequantize(raw, self.info(name)), self.info(name).row_width
230
 
231
 
232
  def _dequantize(raw: bytes, ti: TensorInfo) -> np.ndarray:
 
359
  for d in dims:
360
  n_elements *= int(d)
361
  return n_elements >= QK_K and n_elements % QK_K == 0
362
+
363
+
364
+ def _write_string(f, value: str) -> None:
365
+ data = value.encode("utf-8")
366
+ f.write(struct.pack("<Q", len(data)))
367
+ f.write(data)
368
+
369
+
370
+ def write_quantized_gguf(
371
+ input_path: str | Path,
372
+ output_path: str | Path,
373
+ quantizer,
374
+ imat: dict[str, np.ndarray] | None = None,
375
+ quantize_all: bool = True,
376
+ tensor_names: set[str] | None = None,
377
+ limit: int = 0,
378
+ verbose: bool = True,
379
+ ) -> dict[str, float]:
380
+ """Quantize a source GGUF with the V2 engine and emit a valid GGUF.
381
+
382
+ Eligible tensors become Q2_K; everything else is copied byte-for-byte,
383
+ including the metadata/tokenizer section. Returns aggregate statistics.
384
+ """
385
+ reader = GGUFReader(input_path)
386
+ alignment = reader.alignment
387
+ names = set(tensor_names) if tensor_names else None
388
+ if names is not None:
389
+ missing = names - set(reader._by_name)
390
+ if missing:
391
+ raise KeyError(f"tensors not found: {sorted(missing)}")
392
+
393
+ plan: list[bool] = []
394
+ quantized_count = 0
395
+ for ti in reader.tensors:
396
+ if names is not None:
397
+ want = ti.name in names
398
+ compatible = (
399
+ len(ti.dims) >= 2
400
+ and ti.dims[0] % QK_K == 0
401
+ and ti.n_elements % QK_K == 0
402
+ )
403
+ else:
404
+ want = quantize_all and is_q2k_eligible(ti.name, ti.dims)
405
+ compatible = True
406
+ if want and ti.type not in SUPPORTED_READ_TYPES:
407
+ if verbose:
408
+ print(f" copy (unsupported source {ti.type_name}): {ti.name}")
409
+ want = False
410
+ if want and not compatible:
411
+ if verbose:
412
+ print(f" copy (row width not Q2_K aligned): {ti.name}")
413
+ want = False
414
+ if want and limit and quantized_count >= limit:
415
+ want = False
416
+ if want:
417
+ quantized_count += 1
418
+ plan.append(want)
419
+
420
+ out_infos = []
421
+ data_offset = 0
422
+ for ti, want in zip(reader.tensors, plan):
423
+ if want:
424
+ n_blocks = ti.n_elements // QK_K
425
+ out_type = GGML_TYPE_Q2_K
426
+ out_size = n_blocks * 84
427
+ else:
428
+ out_type = ti.type
429
+ out_size = ti.n_bytes
430
+ out_infos.append((ti, out_type, out_size, data_offset))
431
+ data_offset = _align(data_offset + out_size, alignment)
432
+
433
+ kv_pairs = []
434
+ for key, vtype, raw in reader.kv_pairs:
435
+ if quantized_count and key == "general.file_type" and vtype == 4:
436
+ raw = struct.pack("<I", 10)
437
+ elif quantized_count and key == "general.quantization_version" and vtype == 4:
438
+ raw = struct.pack("<I", 2)
439
+ kv_pairs.append((key, vtype, raw))
440
+
441
+ output_path = Path(output_path)
442
+ output_path.parent.mkdir(parents=True, exist_ok=True)
443
+ total_rmse = 0.0
444
+ total_quantized = 0
445
+ total_elements = 0
446
+ with output_path.open("wb") as fout:
447
+ fout.write(struct.pack("<IIQQ", GGUF_MAGIC, reader.version, len(out_infos), len(kv_pairs)))
448
+ for key, vtype, raw in kv_pairs:
449
+ _write_string(fout, key)
450
+ fout.write(struct.pack("<I", vtype))
451
+ fout.write(raw)
452
+ for ti, out_type, out_size, offset in out_infos:
453
+ _write_string(fout, ti.name)
454
+ fout.write(struct.pack("<I", len(ti.dims)))
455
+ for d in ti.dims:
456
+ fout.write(struct.pack("<Q", int(d)))
457
+ fout.write(struct.pack("<I", out_type))
458
+ fout.write(struct.pack("<Q", offset))
459
+
460
+ data_start = _align(fout.tell(), alignment)
461
+ fout.write(b"\x00" * (data_start - fout.tell()))
462
+
463
+ for idx, (ti, out_type, out_size, offset) in enumerate(out_infos):
464
+ target = data_start + offset
465
+ if target > fout.tell():
466
+ fout.write(b"\x00" * (target - fout.tell()))
467
+ if plan[idx]:
468
+ x, row = reader.read_tensor(ti.name)
469
+ imp = None
470
+ if imat is not None:
471
+ raw_imp = lookup_imatrix(imat, ti.name)
472
+ if raw_imp is not None:
473
+ imp = expand_importance(raw_imp, x.size, row)
474
+ res = quantizer.quantize(x, importance=imp, row_width=row)
475
+ payload = blocks_to_bytes(res.scales, res.qs, res.dm)
476
+ if len(payload) != out_size:
477
+ raise RuntimeError(f"{ti.name}: Q2_K payload size mismatch")
478
+ fout.write(payload)
479
+ total_rmse += res.stats.rmse * res.stats.rmse * x.size
480
+ total_elements += x.size
481
+ total_quantized += 1
482
+ if verbose:
483
+ print(
484
+ f" Q2_K {ti.name:48s} row={row:6d} "
485
+ f"rmse={res.stats.rmse:.6e} imatrix={'yes' if imp is not None else 'no'}"
486
+ )
487
+ else:
488
+ reader.copy_tensor_raw_to(ti.name, fout)
489
+ if verbose and idx % 25 == 0:
490
+ print(f" copy {ti.name}")
491
+
492
+ rmse = float(np.sqrt(total_rmse / total_elements)) if total_elements else 0.0
493
+ return {
494
+ "tensors": float(len(out_infos)),
495
+ "quantized": float(total_quantized),
496
+ "elements": float(total_elements),
497
+ "rmse": rmse,
498
+ }
q2k.py CHANGED
@@ -169,6 +169,20 @@ def blocks_to_arrays(blocks: list[Q2KBlock]) -> tuple[np.ndarray, np.ndarray, np
169
  return scales, qs, dm
170
 
171
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  def dequantize_blocks(blocks: list[Q2KBlock]) -> np.ndarray:
173
  if not blocks:
174
  return np.empty(0, dtype=np.float32)
 
169
  return scales, qs, dm
170
 
171
 
172
+ def blocks_to_bytes(scales: np.ndarray, qs: np.ndarray, dm: np.ndarray) -> bytes:
173
+ """Serialize Q2_K block arrays to GGUF's 84-byte block layout."""
174
+ scales = np.asarray(scales, dtype=np.uint8)
175
+ qs = np.asarray(qs, dtype=np.uint8)
176
+ dm = np.asarray(dm, dtype=np.uint16)
177
+ n = scales.shape[0]
178
+ out = np.empty((n, 84), dtype=np.uint8)
179
+ out[:, :16] = scales
180
+ out[:, 16:80] = qs
181
+ out[:, 80:82] = np.ascontiguousarray(dm[:, 0]).view(np.uint8).reshape(n, 2)
182
+ out[:, 82:84] = np.ascontiguousarray(dm[:, 1]).view(np.uint8).reshape(n, 2)
183
+ return out.tobytes()
184
+
185
+
186
  def dequantize_blocks(blocks: list[Q2KBlock]) -> np.ndarray:
187
  if not blocks:
188
  return np.empty(0, dtype=np.float32)