RegalInvader's picture
Release: Marco-Nano-Instruct 1-, 2- and 3-bit GGUF for phones
e5235fb verified
Raw History Blame Contribute Delete
9.83 kB
#!/usr/bin/env python3
"""PTQ baseline (D-56): what llama.cpp's own post-training quantisation of the teacher reaches at the bit widths of
our QAT students, measured the same way as the students themselves (llama.cpp KL divergence against the bf16 teacher).
Only the routed experts are quantised, as in our students; everything else stays bf16. Each width gets plain
round-to-nearest in the students' own GGUF type (= the QAT starting point) and llama.cpp's best importance-matrix
types near the same bits per weight.
python scripts/ptq_baseline.py texts # eval.txt (held-out eval stream) + calib.txt (calib stream)
python scripts/ptq_baseline.py imatrix # teacher importance matrix on calib.txt (GPU)
python scripts/ptq_baseline.py quantize [NAME..] # the PTQ GGUFs (CPU)
python scripts/ptq_baseline.py base # teacher logits for the KL runs (GPU)
python scripts/ptq_baseline.py kld [NAME..] # KL divergence of each GGUF against the teacher (GPU)
python scripts/ptq_baseline.py report # reports/ptq_baseline.md + .json
Students go in as extra GGUFs: `kld --gguf name=path.gguf`.
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
import numpy as np
REPO = Path(__file__).resolve().parent.parent
DATA = Path("/path/to/data")
OUT = DATA / "ptq"
TEACHER_HF = DATA / "models/Marco-Nano-Instruct"
TEACHER = OUT / "teacher-bf16.gguf"
BIN = Path("/path/to/llama.cpp/build-ptq/bin")
EVAL_SEQS, CALIB_SEQS = 40, 120 # eval: 40 × 4,096 tokens (82K scored); calib: 120 × 4,096 (~490K tokens)
CTX = 4096
EXPS = ("ffn_gate_exps", "ffn_up_exps", "ffn_down_exps")
# name → (gate/up type, down type, uses the importance matrix). down_exps rows are 384 wide, so the 256-block K and
# IQ types cannot hold them; those variants put down in the nearest 32-block type at a similar width instead.
VARIANTS = {
# 4-bit: our int4 student is Q4_0 everywhere
"q4_0-rtn": ("q4_0", "q4_0", False),
"q4_0-imat": ("q4_0", "q4_0", True),
"q4_k-imat": ("q4_k", "q4_0", True),
"iq4_xs-imat": ("iq4_xs", "iq4_nl", True),
# 3-bit: our int3 student is Q3_K gate/up with Q4_0 down
"q3_k-rtn": ("q3_k", "q4_0", False),
"q3_k-imat": ("q3_k", "q4_0", True),
"iq3_s-imat": ("iq3_s", "q4_0", True),
"iq3_xxs-imat": ("iq3_xxs", "q4_0", True),
# 2-bit: our int2 student (and ternary A0) is Q2_0 everywhere
"q2_0-rtn": ("q2_0", "q2_0", False),
"q2_k-imat": ("q2_k", "q2_0", True),
"iq2_s-imat": ("iq2_s", "q2_0", True),
"iq2_xs-imat": ("iq2_xs", "q2_0", True),
"iq2_xxs-imat": ("iq2_xxs", "q2_0", True),
# where the error sits: the same gate/up types with the down projection in non-linear IQ4_NL (32-block, fits 384)
"q4_0-iq4nldown-imat": ("q4_0", "iq4_nl", True),
"iq4_nl-imat": ("iq4_nl", "iq4_nl", True),
"iq4_xs-rtn": ("iq4_xs", "iq4_nl", False),
"q3_k-iq4nldown-imat": ("q3_k", "iq4_nl", True),
"iq3_s-iq4nldown-imat": ("iq3_s", "iq4_nl", True),
"q2_0-imat": ("q2_0", "q2_0", True),
"q2_k-iq4nldown-imat": ("q2_k", "iq4_nl", True),
"iq2_s-iq4nldown-imat": ("iq2_s", "iq4_nl", True),
# Q2N's real competitors (~3.1-3.6 GB): the smaller IQ1/IQ2 codebooks with the down projection in IQ4_NL
"iq2_xs-iq4nldown-imat": ("iq2_xs", "iq4_nl", True),
"iq2_xxs-iq4nldown-imat": ("iq2_xxs", "iq4_nl", True),
"iq1_m-iq4nldown-imat": ("iq1_m", "iq4_nl", True),
"iq1_s-iq4nldown-imat": ("iq1_s", "iq4_nl", True),
"tq1_0-rtn": ("tq1_0", "q2_0", False),
"q2_0-iq4nldown-imat": ("q2_0", "iq4_nl", True),
"tq2_0-rtn": ("tq2_0", "q2_0", False),
# ~1.7-bit: our TQ1_0 repack of A0 is TQ1_0 gate/up with Q2_0 down
"iq1_m-imat": ("iq1_m", "q2_0", True),
"iq1_s-imat": ("iq1_s", "q2_0", True),
}
def run(cmd: list[str], log: Path) -> None:
print(" ".join(map(str, cmd)), flush=True)
with open(log, "w") as f:
r = subprocess.run([str(c) for c in cmd], stdout=f, stderr=subprocess.STDOUT)
if r.returncode:
sys.exit(f"failed ({r.returncode}); see {log}")
def texts(a) -> None:
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained(TEACHER_HF)
for stream, n in (("eval", EVAL_SEQS), ("calib", CALIB_SEQS)):
t = np.memmap(DATA / f"shards/{stream}/tokens.u32", dtype=np.uint32, mode="r")
seqs = [tok.decode(t[i * CTX:(i + 1) * CTX].tolist(), skip_special_tokens=False) for i in range(n)]
(OUT / f"{stream}.txt").write_text("".join(seqs))
print(stream, n, "sequences,", sum(map(len, seqs)), "chars")
def imatrix(a) -> None:
# on a Q8_0 copy of the teacher, the usual practice: the bf16 one (16 GB) does not fit the 16 GB card, and with
# its experts on the CPU a pass took 7 s per 512-token chunk (~2 h for the calib text)
q8 = OUT / "teacher-q8_0.gguf"
if not q8.exists():
run([BIN / "llama-quantize", TEACHER, q8, "q8_0", "16"], OUT / "logs/quantize-q8_0.log")
run([BIN / "llama-imatrix", "-m", q8, "-f", OUT / "calib.txt", "-o", OUT / "imatrix.gguf", "-c", "512",
"-ngl", "99", "-t", "16", "-b", "512"], OUT / "logs/imatrix.log")
def quantize(a) -> None:
(OUT / "gguf").mkdir(exist_ok=True)
for name in a.names or VARIANTS:
gu, down, imat = VARIANTS[name]
dst = OUT / "gguf" / f"teacher-{name}.gguf"
if dst.exists() and not a.force:
continue
cmd = [BIN / "llama-quantize", "--allow-requantize"]
if imat:
cmd += ["--imatrix", OUT / "imatrix.gguf"]
# a bf16 target makes llama-quantize copy every tensor unchanged, overrides included; so the target is Q8_0
# and every 2-D non-expert tensor is pinned back to bf16 (norms and the router are never quantised)
cmd += ["--tensor-type", f"ffn_gate_exps={gu}", "--tensor-type", f"ffn_up_exps={gu}",
"--tensor-type", f"ffn_down_exps={down}"]
for t in ("attn_q", "attn_k", "attn_v", "attn_output"):
cmd += ["--tensor-type", f"{t}=bf16"]
cmd += ["--output-tensor-type", "bf16", "--token-embedding-type", "bf16", TEACHER, dst, "q8_0", "16"]
run(cmd, OUT / f"logs/quantize-{name}.log")
def base(a) -> None:
# the reference stays exact bf16: the experts of layers 0-8 (~4.9 GB) on the CPU, the rest on the card
run([BIN / "llama-perplexity", "-m", TEACHER, "-f", OUT / "eval.txt", "-c", str(CTX), "-ngl", "99",
"-ot", r"blk\.[0-8]\.ffn_.*_exps=CPU", "-t", "16", "--kl-divergence-base", OUT / "teacher.kld"],
OUT / "logs/base.log")
def kld(a) -> None:
targets = {n: OUT / "gguf" / f"teacher-{n}.gguf" for n in (a.names or ([] if a.gguf else VARIANTS))}
extra = dict(g.split("=", 1) for g in a.gguf or [])
if extra: # remembered for the report
reg = OUT / "students.json"
reg.write_text(json.dumps({**(json.loads(reg.read_text()) if reg.exists() else {}), **extra}, indent=1))
targets.update(extra)
for name, path in targets.items():
log = OUT / f"logs/kld-{name}.log"
if log.exists() and "Mean KLD" in log.read_text() and not a.force:
continue
run([BIN / "llama-perplexity", "-m", path, "-c", str(CTX), "-ngl", "99", "-t", "16",
"--kl-divergence-base", OUT / "teacher.kld", "--kl-divergence"], log)
def expert_bpw(path: Path) -> float:
sys.path.insert(0, "/path/to/llama.cpp/gguf-py")
import gguf
r = gguf.GGUFReader(str(path))
b = n = 0
for t in r.tensors:
if any(e in t.name for e in EXPS):
b += int(t.n_bytes)
n += int(np.prod(t.shape))
return 8 * b / n
def report(a) -> None:
rows = []
for log in sorted((OUT / "logs").glob("kld-*.log")):
txt = log.read_text()
name = log.stem[4:]
m = {k: re.search(rf"{k}\s*:\s*([-\d.]+)", txt) for k in ("Mean KLD", "Same top p", "Mean PPL\\(Q\\)")}
if not m["Mean KLD"]:
continue
src = OUT / "gguf" / f"teacher-{name}.gguf"
gg = src if src.exists() else Path(json.loads((OUT / "students.json").read_text())[name])
rows.append({"name": name, "kld": float(m["Mean KLD"].group(1)),
"same_top1_pct": float(m["Same top p"].group(1)) if m["Same top p"] else None,
"ppl": float(m["Mean PPL\\(Q\\)"].group(1)) if m["Mean PPL\\(Q\\)"] else None,
"expert_bpw": round(expert_bpw(gg), 3), "file_gb": round(gg.stat().st_size / 1e9, 2)})
rows.sort(key=lambda r: r["expert_bpw"])
(REPO / "reports/ptq_baseline.json").write_text(json.dumps(rows, indent=1) + "\n")
lines = ["| model | expert bits/weight | file GB | KL vs teacher | same top-1 % | PPL |", "|---|---|---|---|---|---|"]
lines += [f"| {r['name']} | {r['expert_bpw']} | {r['file_gb']} | {r['kld']:.4f} | {r['same_top1_pct']} | "
f"{r['ppl']} |" for r in rows]
print("\n".join(lines))
def main() -> None:
p = argparse.ArgumentParser()
sub = p.add_subparsers(dest="op", required=True)
sub.add_parser("texts")
sub.add_parser("imatrix")
q = sub.add_parser("quantize")
q.add_argument("names", nargs="*")
q.add_argument("--force", action="store_true")
sub.add_parser("base")
k = sub.add_parser("kld")
k.add_argument("names", nargs="*")
k.add_argument("--gguf", action="append", help="name=path of an extra GGUF (a student)")
k.add_argument("--force", action="store_true")
sub.add_parser("report")
a = p.parse_args()
(OUT / "logs").mkdir(parents=True, exist_ok=True)
{"texts": texts, "imatrix": imatrix, "quantize": quantize, "base": base, "kld": kld, "report": report}[a.op](a)
if __name__ == "__main__":
main()