Skip to content

Commit 2161ee0

Browse files
authored
Vibe coded script + constants from mainline + pip requirements (#1405)
1 parent 4d09e04 commit 2161ee0

File tree

4 files changed

+217
-0
lines changed

4 files changed

+217
-0
lines changed

convert_imatrix_gguf_to_dat.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import sys
5+
import logging
6+
import argparse
7+
8+
from pathlib import Path
9+
from dataclasses import dataclass
10+
11+
import numpy as np
12+
import numpy.typing as npt
13+
14+
if 'NO_LOCAL_GGUF' not in os.environ:
15+
sys.path.insert(1, str(Path(__file__).parent / 'gguf-py'))
16+
import gguf
17+
18+
19+
logger = logging.getLogger("gguf-to-imatrix")
20+
21+
22+
def _key_names(attr: str, fallback: str) -> set[str]:
23+
"""Get possible GGUF key names, tolerating missing attributes."""
24+
names = {fallback}
25+
try:
26+
names.add(getattr(gguf.Keys.IMatrix, attr))
27+
except AttributeError:
28+
pass
29+
return names
30+
31+
32+
CHUNK_COUNT_KEYS = _key_names('CHUNK_COUNT', 'imatrix.chunk_count')
33+
CHUNK_SIZE_KEYS = _key_names('CHUNK_SIZE', 'imatrix.chunk_size')
34+
DATASET_KEYS = _key_names('DATASETS', 'imatrix.datasets')
35+
36+
37+
@dataclass
38+
class IMatrixEntry:
39+
values: npt.NDArray[np.float32]
40+
counts: npt.NDArray[np.float32]
41+
42+
43+
class IMatrixDatWriter:
44+
"""Writes the old binary imatrix .dat format."""
45+
46+
def __init__(self, outfile: Path):
47+
self.outfile = outfile
48+
self.chunk_size: int = 512
49+
self.chunk_count: int = 0
50+
self.dataset: str = ""
51+
self.entries: dict[str, IMatrixEntry] = {}
52+
53+
def write(self) -> None:
54+
if self.chunk_size == 0:
55+
raise ValueError("chunk_size is 0, cannot write imatrix")
56+
57+
with open(self.outfile, "wb") as f:
58+
np.array([len(self.entries)], dtype=np.int32).tofile(f)
59+
60+
for name, entry in self.entries.items():
61+
name_bytes = name.encode("utf-8")
62+
np.array([len(name_bytes)], dtype=np.int32).tofile(f)
63+
f.write(name_bytes)
64+
65+
ncall = int(entry.counts[0] / self.chunk_size)
66+
np.array([ncall], dtype=np.int32).tofile(f)
67+
np.array([len(entry.values)], dtype=np.int32).tofile(f)
68+
69+
(entry.values / np.float32(self.chunk_size)).astype(np.float32).tofile(f)
70+
71+
logger.debug(" %s: ncall=%d, nval=%d", name, ncall, len(entry.values))
72+
73+
np.array([self.chunk_count], dtype=np.int32).tofile(f)
74+
75+
dataset_bytes = self.dataset.encode("utf-8")
76+
np.array([len(dataset_bytes)], dtype=np.int32).tofile(f)
77+
if dataset_bytes:
78+
f.write(dataset_bytes)
79+
80+
81+
class GGUFIMatrixReader:
82+
"""Reads imatrix data from a GGUF file."""
83+
84+
SUMS_SUFFIXES = (".sums", ".in_sum2")
85+
COUNTS_SUFFIX = ".counts"
86+
87+
def __init__(self, gguf_path: Path):
88+
reader = gguf.GGUFReader(gguf_path)
89+
90+
self.chunk_count: int = 0
91+
self.chunk_size: int = 512
92+
self.dataset: str = ""
93+
self.entries: dict[str, IMatrixEntry] = {}
94+
95+
# --- Read KV metadata ---
96+
for field in reader.fields.values():
97+
key = field.name
98+
if key in CHUNK_COUNT_KEYS:
99+
val = int(field.parts[field.data[0]][0])
100+
self.chunk_count = val
101+
elif key in CHUNK_SIZE_KEYS:
102+
val = int(field.parts[field.data[0]][0])
103+
self.chunk_size = val
104+
elif key in DATASET_KEYS:
105+
val = bytes(field.parts[field.data[0]]).decode("utf-8")
106+
self.dataset = val
107+
108+
# --- Read all tensors (copy + ensure float32) ---
109+
tensor_map: dict[str, npt.NDArray[np.float32]] = {}
110+
for tensor in reader.tensors:
111+
tensor_map[tensor.name] = np.array(tensor.data, dtype=np.float32)
112+
logger.debug(" Tensor: %s shape=%s", tensor.name, tensor_map[tensor.name].shape)
113+
114+
# --- Match sums/counts pairs ---
115+
sums_tensors: dict[str, npt.NDArray[np.float32]] = {}
116+
counts_tensors: dict[str, npt.NDArray[np.float32]] = {}
117+
118+
for tname, tdata in tensor_map.items():
119+
matched_sum = False
120+
for suffix in self.SUMS_SUFFIXES:
121+
if tname.endswith(suffix):
122+
sums_tensors[tname[:-len(suffix)]] = tdata
123+
matched_sum = True
124+
break
125+
if not matched_sum and tname.endswith(self.COUNTS_SUFFIX):
126+
counts_tensors[tname[:-len(self.COUNTS_SUFFIX)]] = tdata
127+
128+
for name, sums in sums_tensors.items():
129+
counts = counts_tensors.get(name)
130+
if counts is None:
131+
logger.warning("No counts tensor for %r, assuming 0", name)
132+
counts = np.array([0.0], dtype=np.float32)
133+
self.entries[name] = IMatrixEntry(values=sums, counts=counts)
134+
135+
logger.info("Loaded %d imatrix entries from GGUF", len(self.entries))
136+
137+
# --- Diagnostic output if nothing matched ---
138+
if not self.entries:
139+
logger.error("No imatrix tensor pairs found!")
140+
logger.error(
141+
"Expected pairs like '<name>%s' + '<name>%s'",
142+
self.SUMS_SUFFIXES[0], self.COUNTS_SUFFIX
143+
)
144+
if tensor_map:
145+
logger.error("Tensors actually present in the file (%d):", len(tensor_map))
146+
for n in sorted(tensor_map):
147+
logger.error(" %s", n)
148+
else:
149+
logger.error("The file contains no tensors at all.")
150+
logger.error(
151+
"This file may not be a GGUF imatrix, or it may use a "
152+
"naming convention this script doesn't recognize yet."
153+
)
154+
155+
def to_writer(self, outfile: Path) -> IMatrixDatWriter:
156+
writer = IMatrixDatWriter(outfile)
157+
writer.chunk_count = self.chunk_count
158+
writer.chunk_size = self.chunk_size
159+
writer.dataset = self.dataset
160+
writer.entries = self.entries
161+
return writer
162+
163+
164+
def parse_args():
165+
parser = argparse.ArgumentParser(
166+
description="Convert a GGUF imatrix file to the old imatrix.dat format")
167+
parser.add_argument(
168+
"--outfile", type=Path,
169+
help="path to write to; default: based on input.",
170+
)
171+
parser.add_argument(
172+
"--verbose", action="store_true",
173+
help="increase output verbosity",
174+
)
175+
parser.add_argument(
176+
"imatrix", type=Path,
177+
help="path to a GGUF imatrix file",
178+
)
179+
return parser.parse_args()
180+
181+
182+
if __name__ == "__main__":
183+
args = parse_args()
184+
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
185+
186+
if args.outfile is None:
187+
input_file: Path = args.imatrix
188+
if input_file.suffix == ".gguf":
189+
args.outfile = input_file.with_suffix(".dat")
190+
else:
191+
args.outfile = Path(str(input_file) + ".dat")
192+
193+
if args.outfile.exists():
194+
logger.error(
195+
"Default output already exists, use --outfile to overwrite: %s",
196+
args.outfile
197+
)
198+
sys.exit(1)
199+
200+
reader = GGUFIMatrixReader(args.imatrix)
201+
202+
if not reader.entries:
203+
logger.error("Nothing to write (no entries). Re-run with --verbose for details.")
204+
sys.exit(1)
205+
206+
writer = reader.to_writer(args.outfile)
207+
writer.write()
208+
209+
logger.info("Wrote %d entries to %s", len(writer.entries), args.outfile)

gguf-py/gguf/constants.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,11 @@ class Adapter:
186186
TYPE = "adapter.type"
187187
LORA_ALPHA = "adapter.lora.alpha"
188188

189+
class IMatrix:
190+
CHUNK_COUNT = "imatrix.chunk_count"
191+
CHUNK_SIZE = "imatrix.chunk_size"
192+
DATASETS = "imatrix.datasets"
193+
189194
#
190195
# recommended mapping of model tensor names for storage in gguf
191196
#
@@ -194,6 +199,7 @@ class Adapter:
194199
class GGUFType:
195200
MODEL = "model"
196201
ADAPTER = "adapter"
202+
IMATRIX = "imatrix"
197203

198204

199205
class MODEL_ARCH(IntEnum):

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@
1111
-r ./requirements/requirements-convert_llama_ggml_to_gguf.txt
1212
-r ./requirements/requirements-convert_lora_to_gguf.txt
1313
-r ./requirements/requirements-tool_bench.txt
14+
-r ./requirements/requirements-convert_imatrix_gguf_to_dat.txt
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
numpy~=1.26.4

0 commit comments

Comments
 (0)