#!/usr/bin/env python3 # SPDX-License-Identifier: AGPL-3.0-or-later # Copyright (C) 2026 David (Corey) Herrera """gguf_retype.py — retype selected tensors of a GGUF in place-of-copy, leaving every other tensor byte-identical. Why this exists: `llama-quantize --allow-requantize` rebuilds *every* tensor (dequantise -> requantise), so a one-tensor change cannot be isolated from the rounding it does to the other 700. Phase 2 of the batched-decode campaign needs exactly two tensors moved Q5_K -> Q4_K (`blk.0/blk.1.ffn_down_exps.weight`, the only two layers whose `ffn_down_exps` is not a type `gpu_moe_setup` can group) with everything else preserved bit-for-bit. How: the GGUF header + KV block + tensor-info block are copied verbatim; only the `type` (u32) and `offset` (u64) fields of the info records are patched, both fixed-width, so the header length is unchanged. Tensor data is streamed through in info order, copied byte-for-byte except for the retyped tensors, which are dequantised to f32 and requantised with ggml's own `ggml_quantize_chunk` (the same code path `llama-quantize` uses, no imatrix). Requires: libggml-base.so from a llama.cpp build (exports `ggml_quantize_chunk` and `dequantize_row_*`). numpy is NOT required. Usage: # list tensors (name, type, dims, offset, bytes) python3 tools/gguf_retype.py --in model.gguf --dump [--filter ffn_down_exps] # retype python3 tools/gguf_retype.py --in model.gguf --out model.retyped.gguf \ --ggml-lib /path/to/libggml-base.so \ --retype blk.0.ffn_down_exps.weight=Q4_K \ --retype blk.1.ffn_down_exps.weight=Q4_K # re-parse the output and diff the tensor table against the input python3 tools/gguf_retype.py --in model.gguf --verify model.retyped.gguf @status Working; used for Phase 2 (saber blk.0/blk.1 ffn_down_exps Q5_K -> Q4_K) @issues Only the quant types listed in GGML_TYPES below are known to this tool. @todo None """ import argparse import ctypes import os import struct import sys # --- ggml type table: name -> (enum value, block size in elems, bytes/block) -- GGML_TYPES = { "F32": (0, 1, 4), "F16": (1, 1, 2), "Q4_0": (2, 32, 18), "Q4_1": (3, 32, 20), "Q5_0": (6, 32, 22), "Q5_1": (7, 32, 24), "Q8_0": (8, 32, 34), "Q8_1": (9, 32, 36), "Q2_K": (10, 256, 84), "Q3_K": (11, 256, 110), "Q4_K": (12, 256, 144), "Q5_K": (13, 256, 176), "Q6_K": (14, 256, 210), "Q8_K": (15, 256, 292), "BF16": (30, 1, 2), } TYPE_BY_ID = {v[0]: k for k, v in GGML_TYPES.items()} # GGUF metadata value types (U8, I8, U16, I16, U32, I32, F32V, BOOL, STRING, ARRAY, U64, I64, F64) = range(13) _FIXED = {U8: 1, I8: 1, U16: 2, I16: 2, U32: 4, I32: 4, F32V: 4, BOOL: 1, U64: 8, I64: 8, F64: 8} class Reader: def __init__(self, buf): self.b = buf self.p = 0 def read_u32(self): v = struct.unpack_from(" {new_type}, " f"{nrows} rows x {n_per_row} " f"({t.nbytes} -> {nbytes} bytes)") done = 0 while done < nrows: nb = min(rows_per_batch, nrows - done) raw = fi.read(nb * src_rs) if len(raw) != nb * src_rs: sys.exit(f"short read on {t.name}") deq(ctypes.cast(ctypes.c_char_p(raw), ctypes.c_void_p), fbuf, nb * n_per_row) got = lib.ggml_quantize_chunk( GGML_TYPES[new_type][0], fbuf, ctypes.cast(dbuf, ctypes.c_void_p), 0, nb, n_per_row, None) if got != nb * dst_rs: sys.exit(f"quantize_chunk returned {got}, " f"expected {nb * dst_rs}") fo.write(dbuf.raw[:nb * dst_rs]) done += nb written += nbytes if written < off: fo.write(b"\0" * (off - written)) print(f"[retype] wrote {args.out} ({os.path.getsize(args.out)} bytes)") def cmd_verify(args): ha, ta, dsa, aa, va, _ = parse_gguf_metadata(args.inp) hb, tb, dsb, ab, vb, _ = parse_gguf_metadata(args.verify) ok = True if len(ta) != len(tb) or dsa != dsb or aa != ab or va != vb: print(f"HEADER DIFF: n={len(ta)}/{len(tb)} data_start={dsa}/{dsb} " f"align={aa}/{ab} ver={va}/{vb}") ok = False fa = open(args.inp, "rb") fb = open(args.verify, "rb") changed = 0 for x, y in zip(ta, tb): if x.name != y.name or x.dims != y.dims: print(f"TENSOR DIFF: {x.name} vs {y.name}") ok = False continue if x.type != y.type: changed += 1 print(f" retyped {x.name}: {x.type} -> {y.type} " f"({x.nbytes} -> {y.nbytes} bytes)") continue if x.nbytes != y.nbytes: print(f"SIZE DIFF: {x.name}") ok = False continue # byte-compare the payload fa.seek(dsa + x.offset) fb.seek(dsb + y.offset) left = x.nbytes while left: n = min(8 << 20, left) if fa.read(n) != fb.read(n): print(f"BYTE DIFF in unchanged tensor {x.name}") ok = False break left -= n fa.close() fb.close() # metadata block: identical except the patched type/offset fields diffs = sum(1 for i in range(min(len(ha), len(hb))) if ha[i] != hb[i]) print(f"metadata bytes differing: {diffs} " f"(expected: type+offset fields only)") print(f"retyped tensors: {changed}") print("VERIFY: " + ("PASS" if ok else "FAIL")) return 0 if ok else 1 def main(): ap = argparse.ArgumentParser() ap.add_argument("--in", dest="inp", required=True) ap.add_argument("--out") ap.add_argument("--dump", action="store_true") ap.add_argument("--filter", default=None) ap.add_argument("--verify", default=None, help="output file to check") ap.add_argument("--retype", action="append", default=[], help="name=TYPE, repeatable") ap.add_argument("--ggml-lib", default=os.environ.get("GGML_LIB"), help="path to libggml-base.so (default: $GGML_LIB)") a = ap.parse_args() if a.dump: return cmd_dump(a) if a.verify: return cmd_verify(a) if not a.out or not a.retype: ap.error("need --out and at least one --retype (or --dump/--verify)") if not a.ggml_lib: ap.error("retype needs --ggml-lib or GGML_LIB (libggml-base.so)") return cmd_retype(a) if __name__ == "__main__": sys.exit(main() or 0)