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

Upload 12 files

Browse files
Files changed (3) hide show
  1. __init__.py +5 -0
  2. __main__.py +201 -28
  3. gguf.py +326 -0
__init__.py CHANGED
@@ -14,6 +14,7 @@ from .factored import (
14
  factor_only_solve,
15
  factor_q2k_block,
16
  )
 
17
  from .pipeline import Q2KQuantizer, QuantizedTensor, TensorStats
18
  from .q2k import Q2KBlock, QK_K, dequantize_params, pack_codes, unpack_codes
19
 
@@ -30,6 +31,10 @@ __all__ = [
30
  "factor_candidates",
31
  "factor_only_solve",
32
  "factor_q2k_block",
 
 
 
 
33
  "pack_codes",
34
  "unpack_codes",
35
  ]
 
14
  factor_only_solve,
15
  factor_q2k_block,
16
  )
17
+ from .gguf import GGUFReader, expand_importance, lookup_imatrix, read_imatrix
18
  from .pipeline import Q2KQuantizer, QuantizedTensor, TensorStats
19
  from .q2k import Q2KBlock, QK_K, dequantize_params, pack_codes, unpack_codes
20
 
 
31
  "factor_candidates",
32
  "factor_only_solve",
33
  "factor_q2k_block",
34
+ "GGUFReader",
35
+ "expand_importance",
36
+ "lookup_imatrix",
37
+ "read_imatrix",
38
  "pack_codes",
39
  "unpack_codes",
40
  ]
__main__.py CHANGED
@@ -1,9 +1,14 @@
1
- """Command-line entry point for the V2 factored Q2_K quantizer.
2
 
3
  Examples::
4
 
5
  python -m hpc_quantize_v2 --demo
6
  python -m hpc_quantize_v2 --input tensor.npy --output tensor_q2k.npz
 
 
 
 
 
7
  """
8
 
9
  from __future__ import annotations
@@ -13,14 +18,180 @@ import argparse
13
  import numpy as np
14
 
15
  from .config import HPCConfig
 
 
 
 
 
 
 
 
16
  from .pipeline import Q2KQuantizer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
 
19
  def main() -> None:
20
  parser = argparse.ArgumentParser(description="HPC-Quantize V2 factored Q2_K")
21
- parser.add_argument("--input", help=".npy file containing float weights")
22
- parser.add_argument("--importance", help="optional .npy importance matrix")
23
- parser.add_argument("--output", help="output .npz with scales/qs/dm")
 
 
 
 
 
 
 
 
 
 
24
  parser.add_argument("--demo", action="store_true", help="quantize synthetic weights")
25
  parser.add_argument("--blocks", type=int, default=64, help="demo block count")
26
  parser.add_argument("--std", type=float, default=0.02, help="demo weight std")
@@ -28,39 +199,41 @@ def main() -> None:
28
  parser.add_argument("--cand-d", type=int, default=None, help="candidate d count")
29
  parser.add_argument("--cand-m", type=int, default=None, help="candidate min count")
30
  parser.add_argument("--mse", action="store_true", help="disable HPC sequence search")
 
31
  parser.add_argument("--verbose", action="store_true")
 
 
32
  args = parser.parse_args()
33
 
 
 
 
 
 
 
 
 
 
 
34
  if args.demo:
35
  rng = np.random.default_rng(args.seed)
36
  weights = rng.normal(0.0, args.std, size=(args.blocks, 256)).astype(np.float32)
37
  importance = None
38
- elif args.input:
39
- weights = np.load(args.input)
40
- importance = np.load(args.importance) if args.importance else None
41
- else:
42
- parser.error("pass --demo or --input")
 
 
 
 
43
 
44
- cfg = HPCConfig(verbose=True)
45
- if args.cand_d is not None:
46
- cfg.n_cand_d = args.cand_d
47
- if args.cand_m is not None:
48
- cfg.n_cand_m = args.cand_m
49
- if args.mse:
50
- cfg.optimizer = "mse"
51
 
52
- result = Q2KQuantizer(cfg).quantize(weights, importance=importance)
53
- if args.output:
54
- np.savez_compressed(
55
- args.output,
56
- scales=result.scales,
57
- qs=result.qs,
58
- dm=result.dm,
59
- dequantized=result.dequantize(),
60
- )
61
- print(f"wrote {args.output}")
62
- else:
63
- _ = result.dequantize()
64
 
65
 
66
  if __name__ == "__main__":
 
1
+ """Command-line entry point for HPC-Quantize V2.
2
 
3
  Examples::
4
 
5
  python -m hpc_quantize_v2 --demo
6
  python -m hpc_quantize_v2 --input tensor.npy --output tensor_q2k.npz
7
+ python -m hpc_quantize_v2 --gguf model.gguf --list-tensors
8
+ python -m hpc_quantize_v2 --gguf model.gguf --tensor blk.0.ffn_down.weight \
9
+ --imatrix-gguf imatrix.gguf --output ffn_down_q2k.npz
10
+ python -m hpc_quantize_v2 --gguf model.gguf --all-q2k \
11
+ --imatrix-gguf imatrix.gguf --output model_q2k_arrays.npz
12
  """
13
 
14
  from __future__ import annotations
 
18
  import numpy as np
19
 
20
  from .config import HPCConfig
21
+ from .factored import factor_only_solve
22
+ from .gguf import (
23
+ GGUFReader,
24
+ expand_importance,
25
+ is_q2k_eligible,
26
+ lookup_imatrix,
27
+ read_imatrix,
28
+ )
29
  from .pipeline import Q2KQuantizer
30
+ from .reference import reference_solve, synthetic_cases
31
+
32
+
33
+ def _config_from_args(args) -> HPCConfig:
34
+ cfg = HPCConfig(verbose=args.verbose)
35
+ if args.cand_d is not None:
36
+ cfg.n_cand_d = args.cand_d
37
+ if args.cand_m is not None:
38
+ cfg.n_cand_m = args.cand_m
39
+ if args.mse:
40
+ cfg.optimizer = "mse"
41
+ if args.no_spectral:
42
+ cfg.use_spectral = False
43
+ return cfg.validate()
44
+
45
+
46
+ def _importance_for(imat, name, n_elements, row_width):
47
+ if imat is None:
48
+ return None
49
+ raw = lookup_imatrix(imat, name)
50
+ if raw is None:
51
+ return None
52
+ return expand_importance(raw, n_elements, row_width)
53
+
54
+
55
+ def _save_result(path: str, res, prefix: str = "", extra: dict | None = None) -> None:
56
+ payload = {
57
+ f"{prefix}scales": res.scales,
58
+ f"{prefix}qs": res.qs,
59
+ f"{prefix}dm": res.dm,
60
+ f"{prefix}dequantized": res.dequantize(),
61
+ f"{prefix}rmse": np.float32(res.stats.rmse),
62
+ }
63
+ if extra:
64
+ payload.update(extra)
65
+ np.savez_compressed(path, **payload)
66
+
67
+
68
+ def _run_gguf(args, cfg: HPCConfig) -> None:
69
+ reader = GGUFReader(args.gguf)
70
+ imat = None
71
+ imatrix_path = args.imatrix_gguf or args.imatrix
72
+ if imatrix_path:
73
+ imat = read_imatrix(imatrix_path)
74
+ print(f"iMatrix: {len(imat)} tensors from {imatrix_path}")
75
+
76
+ if args.list_tensors or (not args.tensor and not args.all_q2k):
77
+ print(f"{reader.path}: {len(reader.tensors)} tensors, {len(reader.metadata)} metadata keys")
78
+ for ti in reader.tensors:
79
+ tag = "Q2K" if is_q2k_eligible(ti.name, ti.dims) else " "
80
+ print(
81
+ f" {tag} {ti.name:48s} {ti.type_name:6s} "
82
+ f"dims={ti.dims} row={ti.row_width}"
83
+ )
84
+ return
85
+
86
+ quantizer = Q2KQuantizer(cfg)
87
+ if args.all_q2k:
88
+ payload: dict[str, np.ndarray] = {}
89
+ done = 0
90
+ for ti in reader.tensors:
91
+ if not is_q2k_eligible(ti.name, ti.dims):
92
+ continue
93
+ if args.limit and done >= args.limit:
94
+ break
95
+ x, row = reader.read_tensor(ti.name)
96
+ if args.rows:
97
+ x = x[: args.rows * row]
98
+ imp = _importance_for(imat, ti.name, x.size, row)
99
+ res = quantizer.quantize(x, importance=imp, row_width=row)
100
+ prefix = ti.name.replace("/", "_") + "__"
101
+ payload[f"{prefix}scales"] = res.scales
102
+ payload[f"{prefix}qs"] = res.qs
103
+ payload[f"{prefix}dm"] = res.dm
104
+ payload[f"{prefix}rmse"] = np.float32(res.stats.rmse)
105
+ print(
106
+ f" {ti.name:48s} row={row:6d} rmse={res.stats.rmse:.6e} "
107
+ f"imatrix={'yes' if imp is not None else 'no'}"
108
+ )
109
+ done += 1
110
+ if args.output:
111
+ np.savez_compressed(args.output, **payload)
112
+ print(f"wrote {args.output} ({done} tensors)")
113
+ return
114
+
115
+ ti = reader.info(args.tensor)
116
+ x, row = reader.read_tensor(ti.name)
117
+ if args.rows:
118
+ x = x[: args.rows * row]
119
+ imp = _importance_for(imat, ti.name, x.size, row)
120
+ res = quantizer.quantize(x, importance=imp, row_width=row)
121
+ print(
122
+ f"{ti.name}: type={ti.type_name} row={row} blocks={len(res.blocks)} "
123
+ f"rmse={res.stats.rmse:.6e} imatrix={'yes' if imp is not None else 'no'}"
124
+ )
125
+ if args.output:
126
+ _save_result(
127
+ args.output,
128
+ res,
129
+ extra={
130
+ "name": np.array(ti.name),
131
+ "row_width": np.int64(row),
132
+ "source_type": np.array(ti.type_name),
133
+ },
134
+ )
135
+ print(f"wrote {args.output}")
136
+
137
+
138
+ def _run_numpy(args, cfg: HPCConfig) -> None:
139
+ weights = np.load(args.input)
140
+ importance = np.load(args.importance) if args.importance else None
141
+ if importance is None and (args.imatrix_gguf or args.imatrix):
142
+ if not args.tensor_name:
143
+ raise SystemExit("--tensor-name is required to select an iMatrix entry for .npy input")
144
+ imat = read_imatrix(args.imatrix_gguf or args.imatrix)
145
+ raw = lookup_imatrix(imat, args.tensor_name)
146
+ if raw is None:
147
+ raise SystemExit(f"iMatrix entry not found: {args.tensor_name}")
148
+ row = int(weights.shape[-1]) if weights.ndim >= 2 else int(weights.size)
149
+ importance = expand_importance(raw, weights.size, row)
150
+ res = Q2KQuantizer(cfg).quantize(weights, importance=importance)
151
+ if args.output:
152
+ _save_result(args.output, res)
153
+ print(f"wrote {args.output}")
154
+ else:
155
+ _ = res.dequantize()
156
+
157
+
158
+ def _bench(per_kind: int) -> None:
159
+ from collections import defaultdict
160
+
161
+ cases = synthetic_cases(123, per_kind)
162
+ for use_spectral in (False, True):
163
+ cfg = HPCConfig(use_spectral=use_spectral)
164
+ gains = defaultdict(list)
165
+ for kind, x in cases:
166
+ w = np.ones(x.size, dtype=np.float32)
167
+ v2 = factor_only_solve(x, w, cfg)
168
+ ref_obj, _ = reference_solve(x, w, cfg, use_spectral)
169
+ gains[kind].append((ref_obj - v2.objective) / max(abs(ref_obj), 1.0e-30))
170
+ print(f"spectral={use_spectral}")
171
+ for kind, vals in gains.items():
172
+ arr = np.asarray(vals)
173
+ print(
174
+ f" {kind:14s} mean={arr.mean()*100:6.2f}% "
175
+ f"median={np.median(arr)*100:6.2f}% min={arr.min()*100:6.2f}% "
176
+ f"worse={int(np.sum(arr < -1e-9))}/{arr.size}"
177
+ )
178
 
179
 
180
  def main() -> None:
181
  parser = argparse.ArgumentParser(description="HPC-Quantize V2 factored Q2_K")
182
+ source = parser.add_mutually_exclusive_group()
183
+ source.add_argument("--input", help=".npy file containing float weights")
184
+ source.add_argument("--gguf", help="source GGUF file")
185
+ parser.add_argument("--tensor", help="GGUF tensor name to quantize")
186
+ parser.add_argument("--tensor-name", help="name used to look up an iMatrix entry for .npy input")
187
+ parser.add_argument("--list-tensors", action="store_true", help="list GGUF tensors and exit")
188
+ parser.add_argument("--all-q2k", action="store_true", help="quantize all eligible GGUF tensors")
189
+ parser.add_argument("--limit", type=int, default=0, help="limit tensors in --all-q2k")
190
+ parser.add_argument("--rows", type=int, default=0, help="limit rows for a single GGUF tensor")
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")
 
199
  parser.add_argument("--cand-d", type=int, default=None, help="candidate d count")
200
  parser.add_argument("--cand-m", type=int, default=None, help="candidate min count")
201
  parser.add_argument("--mse", action="store_true", help="disable HPC sequence search")
202
+ parser.add_argument("--no-spectral", action="store_true", help="disable spectral/DC objective")
203
  parser.add_argument("--verbose", action="store_true")
204
+ parser.add_argument("--bench", action="store_true", help="measure solver gains vs C port")
205
+ parser.add_argument("--bench-per-kind", type=int, default=12)
206
  args = parser.parse_args()
207
 
208
+ cfg = _config_from_args(args)
209
+
210
+ if args.bench:
211
+ _bench(args.bench_per_kind)
212
+ return
213
+
214
+ if args.gguf:
215
+ _run_gguf(args, cfg)
216
+ return
217
+
218
  if args.demo:
219
  rng = np.random.default_rng(args.seed)
220
  weights = rng.normal(0.0, args.std, size=(args.blocks, 256)).astype(np.float32)
221
  importance = None
222
+ if args.imatrix_gguf or args.imatrix:
223
+ imat = read_imatrix(args.imatrix_gguf or args.imatrix)
224
+ name = args.tensor_name or next(iter(imat))
225
+ importance = expand_importance(lookup_imatrix(imat, name), weights.size, int(weights.shape[-1]))
226
+ res = Q2KQuantizer(cfg).quantize(weights, importance=importance)
227
+ if args.output:
228
+ _save_result(args.output, res)
229
+ print(f"wrote {args.output}")
230
+ return
231
 
232
+ if args.input:
233
+ _run_numpy(args, cfg)
234
+ return
 
 
 
 
235
 
236
+ parser.error("pass --demo, --input, --gguf or --bench")
 
 
 
 
 
 
 
 
 
 
 
237
 
238
 
239
  if __name__ == "__main__":
gguf.py ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal GGUF reader and llama.cpp iMatrix loader.
2
+
3
+ The V2 quantizer works on float tensors, so GGUF ingestion is deliberately a
4
+ thin, read-only layer:
5
+
6
+ * parse the GGUF header, metadata and tensor directory;
7
+ * dequantize the common source types (F32, F16, BF16, Q8_0, Q4_0 and Q2_K);
8
+ * read llama.cpp ``.gguf``/legacy ``.dat`` importance matrices and normalize
9
+ each vector to mean 1.0, matching ``imatrix_reader.h``.
10
+
11
+ No GGUF writer is provided here; the quantizer emits Q2_K arrays that can be
12
+ packed by an external GGUF writer.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import struct
18
+ from dataclasses import dataclass
19
+ 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
27
+
28
+ GGML_TYPE_F32 = 0
29
+ GGML_TYPE_F16 = 1
30
+ GGML_TYPE_Q4_0 = 2
31
+ GGML_TYPE_Q4_1 = 3
32
+ GGML_TYPE_Q5_0 = 6
33
+ GGML_TYPE_Q5_1 = 7
34
+ GGML_TYPE_Q8_0 = 8
35
+ GGML_TYPE_Q2_K = 10
36
+ GGML_TYPE_BF16 = 30
37
+
38
+ TYPE_NAME = {
39
+ GGML_TYPE_F32: "F32",
40
+ GGML_TYPE_F16: "F16",
41
+ GGML_TYPE_Q4_0: "Q4_0",
42
+ GGML_TYPE_Q4_1: "Q4_1",
43
+ GGML_TYPE_Q5_0: "Q5_0",
44
+ GGML_TYPE_Q5_1: "Q5_1",
45
+ GGML_TYPE_Q8_0: "Q8_0",
46
+ GGML_TYPE_Q2_K: "Q2_K",
47
+ GGML_TYPE_BF16: "BF16",
48
+ }
49
+
50
+ BLOCK_SIZE = {
51
+ GGML_TYPE_F32: 1,
52
+ GGML_TYPE_F16: 1,
53
+ GGML_TYPE_BF16: 1,
54
+ GGML_TYPE_Q4_0: 32,
55
+ GGML_TYPE_Q4_1: 32,
56
+ GGML_TYPE_Q5_0: 32,
57
+ GGML_TYPE_Q5_1: 32,
58
+ GGML_TYPE_Q8_0: 32,
59
+ GGML_TYPE_Q2_K: QK_K,
60
+ }
61
+ BLOCK_BYTES = {
62
+ GGML_TYPE_F32: 4,
63
+ GGML_TYPE_F16: 2,
64
+ GGML_TYPE_BF16: 2,
65
+ GGML_TYPE_Q4_0: 18,
66
+ GGML_TYPE_Q4_1: 20,
67
+ GGML_TYPE_Q5_0: 22,
68
+ GGML_TYPE_Q5_1: 24,
69
+ GGML_TYPE_Q8_0: 34,
70
+ GGML_TYPE_Q2_K: 84,
71
+ }
72
+
73
+
74
+ @dataclass(slots=True)
75
+ class TensorInfo:
76
+ name: str
77
+ dims: tuple[int, ...]
78
+ type: int
79
+ offset: int
80
+
81
+ @property
82
+ def n_elements(self) -> int:
83
+ n = 1
84
+ for d in self.dims:
85
+ n *= int(d)
86
+ return n
87
+
88
+ @property
89
+ def row_width(self) -> int:
90
+ return int(self.dims[0]) if self.dims else 1
91
+
92
+ @property
93
+ def type_name(self) -> str:
94
+ return TYPE_NAME.get(self.type, f"type_{self.type}")
95
+
96
+ @property
97
+ def n_bytes(self) -> int:
98
+ if self.type not in BLOCK_SIZE:
99
+ raise ValueError(f"unsupported GGUF type {self.type} ({self.type_name})")
100
+ block = BLOCK_SIZE[self.type]
101
+ if self.n_elements % block != 0:
102
+ raise ValueError(f"{self.name}: element count is not block-aligned")
103
+ return (self.n_elements // block) * BLOCK_BYTES[self.type]
104
+
105
+
106
+ def _align(value: int, alignment: int = DEFAULT_ALIGNMENT) -> int:
107
+ return (value + alignment - 1) & ~(alignment - 1)
108
+
109
+
110
+ def _read_string(f) -> str:
111
+ n = struct.unpack("<Q", f.read(8))[0]
112
+ return f.read(n).decode("utf-8", errors="replace")
113
+
114
+
115
+ def _read_value(f, vtype: int):
116
+ if vtype == 0:
117
+ return struct.unpack("<B", f.read(1))[0]
118
+ if vtype == 1:
119
+ return struct.unpack("<b", f.read(1))[0]
120
+ if vtype == 2:
121
+ return struct.unpack("<H", f.read(2))[0]
122
+ if vtype == 3:
123
+ return struct.unpack("<h", f.read(2))[0]
124
+ if vtype == 4:
125
+ return struct.unpack("<I", f.read(4))[0]
126
+ if vtype == 5:
127
+ return struct.unpack("<i", f.read(4))[0]
128
+ if vtype == 6:
129
+ return struct.unpack("<f", f.read(4))[0]
130
+ if vtype == 7:
131
+ return bool(struct.unpack("<B", f.read(1))[0])
132
+ if vtype == 8:
133
+ return _read_string(f)
134
+ if vtype == 9:
135
+ subtype = struct.unpack("<I", f.read(4))[0]
136
+ length = struct.unpack("<Q", f.read(8))[0]
137
+ if subtype == 8:
138
+ for _ in range(length):
139
+ _read_string(f)
140
+ else:
141
+ sizes = {0: 1, 1: 1, 2: 2, 3: 2, 4: 4, 5: 4, 6: 4, 7: 1, 10: 8, 11: 8, 12: 8}
142
+ f.read(sizes.get(subtype, 4) * length)
143
+ return None
144
+ if vtype == 10:
145
+ return struct.unpack("<Q", f.read(8))[0]
146
+ if vtype == 11:
147
+ return struct.unpack("<q", f.read(8))[0]
148
+ if vtype == 12:
149
+ return struct.unpack("<d", f.read(8))[0]
150
+ raise ValueError(f"unknown GGUF value type {vtype}")
151
+
152
+
153
+ 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]
171
+ dims = tuple(struct.unpack("<Q", f.read(8))[0] for _ in range(n_dims))
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:
179
+ try:
180
+ return self._by_name[name]
181
+ except KeyError:
182
+ raise KeyError(f"tensor '{name}' not found in {self.path}") from None
183
+
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:
198
+ t = ti.type
199
+ if t == GGML_TYPE_F32:
200
+ return np.frombuffer(raw, dtype="<f4").astype(np.float32)
201
+ if t == GGML_TYPE_F16:
202
+ return np.frombuffer(raw, dtype="<f2").astype(np.float32)
203
+ if t == GGML_TYPE_BF16:
204
+ bits = np.frombuffer(raw, dtype="<u2").astype(np.uint32) << 16
205
+ return bits.view(np.float32)
206
+ if t == GGML_TYPE_Q8_0:
207
+ blocks = np.frombuffer(raw, dtype=np.uint8).reshape(-1, 34)
208
+ d = blocks[:, :2].copy().view("<f2").reshape(-1).astype(np.float32)
209
+ q = blocks[:, 2:].view(np.int8).astype(np.float32)
210
+ return (d[:, None] * q).reshape(-1)
211
+ if t == GGML_TYPE_Q4_0:
212
+ blocks = np.frombuffer(raw, dtype=np.uint8).reshape(-1, 18)
213
+ d = blocks[:, :2].copy().view("<f2").reshape(-1).astype(np.float32)
214
+ qs = blocks[:, 2:]
215
+ low = (qs & 0x0F).astype(np.float32) - 8.0
216
+ high = (qs >> 4).astype(np.float32) - 8.0
217
+ q = np.concatenate([low, high], axis=1)
218
+ return (d[:, None] * q).reshape(-1)
219
+ if t == GGML_TYPE_Q2_K:
220
+ blocks = np.frombuffer(raw, dtype=np.uint8).reshape(-1, 84)
221
+ scales = blocks[:, :16]
222
+ qs = blocks[:, 16:80]
223
+ d = blocks[:, 80:82].copy().view("<f2").reshape(-1).astype(np.float32)
224
+ dmin = blocks[:, 82:84].copy().view("<f2").reshape(-1).astype(np.float32)
225
+ codes = np.stack([unpack_codes(q) for q in qs])
226
+ return dequantize_params_batch(d, dmin, scales & 0x0F, scales >> 4, codes).reshape(-1)
227
+ raise ValueError(f"unsupported source GGUF type {ti.type_name} for {ti.name}")
228
+
229
+
230
+ def read_imatrix(path: str | Path) -> dict[str, np.ndarray]:
231
+ """Read a llama.cpp imatrix (GGUF) or legacy .dat file.
232
+
233
+ Returns ``name -> float32 vector`` normalized to mean 1.0, exactly the
234
+ per-input-column ``E[a^2]`` weight used by the C engine.
235
+ """
236
+ path = Path(path)
237
+ imat: dict[str, np.ndarray] = {}
238
+ with path.open("rb") as f:
239
+ magic = struct.unpack("<I", f.read(4))[0]
240
+ if magic == GGUF_MAGIC:
241
+ f.seek(0)
242
+ reader = GGUFReader(path)
243
+ sums: dict[str, np.ndarray] = {}
244
+ counts: dict[str, np.ndarray] = {}
245
+ with reader.path.open("rb") as fh:
246
+ for ti in reader.tensors:
247
+ if not (ti.name.endswith(".in_sum2") or ti.name.endswith(".counts")):
248
+ continue
249
+ fh.seek(reader.data_start + ti.offset)
250
+ raw = fh.read(ti.n_bytes)
251
+ data = np.frombuffer(raw, dtype="<f4").astype(np.float32)
252
+ if ti.name.endswith(".in_sum2"):
253
+ sums[ti.name[: -len(".in_sum2")]] = data
254
+ else:
255
+ counts[ti.name[: -len(".counts")]] = data
256
+ for base, sum2 in sums.items():
257
+ c = counts.get(base)
258
+ if c is not None and c.size and float(c.flat[0]) > 0.0:
259
+ values = sum2 / float(c.flat[0])
260
+ else:
261
+ values = np.ones_like(sum2)
262
+ mean = float(values.mean())
263
+ imat[base] = values / mean if mean > 1.0e-30 else np.ones_like(values)
264
+ else:
265
+ f.seek(0)
266
+ n_entries = struct.unpack("<i", f.read(4))[0]
267
+ for _ in range(n_entries):
268
+ name_len = struct.unpack("<i", f.read(4))[0]
269
+ name = f.read(name_len).decode("utf-8", errors="replace")
270
+ n_values = struct.unpack("<i", f.read(4))[0]
271
+ struct.unpack("<i", f.read(4))[0]
272
+ values = np.frombuffer(f.read(n_values * 4), dtype="<f4").astype(np.float32)
273
+ mean = float(values.mean())
274
+ imat[name] = values / mean if mean > 1.0e-30 else np.ones_like(values)
275
+ return imat
276
+
277
+
278
+ def lookup_imatrix(imat: dict[str, np.ndarray], name: str) -> np.ndarray | None:
279
+ if name in imat:
280
+ return imat[name]
281
+ for suffix in (".weight", ".in_sum2", ".counts"):
282
+ alt = name[: -len(suffix)] if name.endswith(suffix) else name + suffix
283
+ if alt in imat:
284
+ return imat[alt]
285
+ return None
286
+
287
+
288
+ def expand_importance(
289
+ values: np.ndarray | None, n_elements: int, row_width: int
290
+ ) -> np.ndarray | None:
291
+ if values is None:
292
+ return None
293
+ v = np.asarray(values, dtype=np.float32).ravel()
294
+ if v.size == n_elements:
295
+ return v
296
+ if row_width > 0 and v.size == row_width and n_elements % row_width == 0:
297
+ return np.tile(v, n_elements // row_width)
298
+ raise ValueError(
299
+ f"importance length {v.size} does not match {n_elements} elements "
300
+ f"(row width {row_width})"
301
+ )
302
+
303
+
304
+ def is_q2k_eligible(name: str, dims: tuple[int, ...]) -> bool:
305
+ if len(dims) < 2:
306
+ return False
307
+ if dims[0] % QK_K != 0:
308
+ return False
309
+ if "token_embd" in name or name == "output.weight":
310
+ return False
311
+ if "norm" in name or ".bias" in name:
312
+ return False
313
+ if "ffn_gate_inp" in name:
314
+ return False
315
+ if name.startswith("mm.") or name.startswith("a.") or name.startswith("v."):
316
+ return False
317
+ if "altup" in name or "laurel" in name or "layer_output_scale" in name:
318
+ return False
319
+ if "ssm_a" in name or "ssm_dt" in name or "dt_bias" in name:
320
+ return False
321
+ if "ssm_conv1d" in name:
322
+ return False
323
+ n_elements = 1
324
+ for d in dims:
325
+ n_elements *= int(d)
326
+ return n_elements >= QK_K and n_elements % QK_K == 0