Upload 12 files
Browse files- __init__.py +5 -0
- __main__.py +201 -28
- 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
|
| 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 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 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 |
-
|
| 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
|