File size: 7,324 Bytes
28c70af
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT OR Apache-2.0
"""The oracle of `bankml convert` (O5): llama.cpp b11192's own converter on the same safetensors directory.

    convert_oracle.py --record DIR OUT.gguf      llama.cpp's `convert_hf_to_gguf.py DIR --outtype f16 --outfile OUT.gguf`
                                                 (needs the b11192 source tree in $BANKML_LLAMA_SRC and its Python
                                                 requirements: torch CPU, transformers, numpy, safetensors)
    convert_oracle.py --bankml DIR ORACLE.gguf   `bankml convert DIR` and the sha256 of both files: identical or not
    convert_oracle.py --compare A.gguf B.gguf    where two GGUFs differ: header, each key (order, type, value), each
                                                 tensor (name, order, type, shape, offset, sha256 of its data)
    convert_oracle.py --chkhsh DIR               llama.cpp's pre-tokenizer hash of DIR's tokenizer (its get_vocab_base_pre
                                                 rule, through transformers) and bankml's vocabulary+merges sha256: the
                                                 evidence a KNOWN_TOKENIZERS entry in convert.rs needs
    convert_oracle.py --names OUT.jsonl          gguf-py's name heuristics on a corpus of model ids, for the Rust test
                                                 oracle_name_heuristics (BANKML_NAMES_ORACLE=OUT.jsonl)

The directory's name matters: llama.cpp names the model from it (general.name, basename, finetune, size label), so
record and compare the same directory. Byte identity of the two files is the bar; --compare explains a miss.
"""
import ast, hashlib, json, os, re, subprocess, sys
from pathlib import Path

HERE = Path(__file__).resolve().parent
ROOT = HERE.parent
SRC = Path(os.environ.get("BANKML_LLAMA_SRC", "upstream/llama.cpp"))


def sha256(p):
    h = hashlib.sha256()
    with open(p, "rb") as f:
        for b in iter(lambda: f.read(1 << 20), b""):
            h.update(b)
    return h.hexdigest()


def gguf_mod():
    sys.path.insert(0, str(SRC / "gguf-py"))
    import gguf  # noqa: E402
    return gguf


def record(d, out):
    conv = SRC / "convert_hf_to_gguf.py"
    if not conv.is_file():
        sys.exit(f"no {conv}: set BANKML_LLAMA_SRC to a llama.cpp b11192 source tree")
    subprocess.run([sys.executable, str(conv), d, "--outtype", "f16", "--outfile", out], check=True)
    print(f"{sha256(out)}  {out}  (llama.cpp convert_hf_to_gguf.py --outtype f16)")


def bankml(d, oracle):
    out = str(Path(oracle).with_suffix(".bankml.gguf"))
    exe = os.environ.get("BANKML", str(ROOT / "target/release/bankml"))
    subprocess.run([exe, "convert", d, "-o", out], check=True)
    a, b = sha256(out), sha256(oracle)
    print(f"bankml    {a}\nllama.cpp {b}\n{'IDENTICAL' if a == b else 'DIFFERENT (run --compare)'}")
    if a == b:
        os.remove(out)
    sys.exit(0 if a == b else 1)


def compare(a, b):
    g = gguf_mod()
    ra, rb = g.GGUFReader(a), g.GGUFReader(b)
    def kv(r):
        out = []
        for f in r.fields.values():
            v = [bytes(f.parts[i]).hex() for i in f.data]
            out.append((f.name, [int(t) for t in f.types], hashlib.sha256("".join(v).encode()).hexdigest()[:16]))
        return out
    ka, kb = kv(ra), kv(rb)
    bad = 0
    for i in range(max(len(ka), len(kb))):
        x = ka[i] if i < len(ka) else None
        y = kb[i] if i < len(kb) else None
        if x != y:
            bad += 1
            print(f"key #{i}: {x}  !=  {y}")
    for i in range(max(len(ra.tensors), len(rb.tensors))):
        x = ra.tensors[i] if i < len(ra.tensors) else None
        y = rb.tensors[i] if i < len(rb.tensors) else None
        d = lambda t: None if t is None else (t.name, int(t.tensor_type), [int(s) for s in t.shape], int(t.data_offset), hashlib.sha256(bytes(t.data)).hexdigest()[:16])
        if d(x) != d(y):
            bad += 1
            print(f"tensor #{i}: {d(x)}  !=  {d(y)}")
    print(f"{len(ka)} / {len(kb)} keys, {len(ra.tensors)} / {len(rb.tensors)} tensors, {bad} differences")
    sys.exit(1 if bad else 0)


def chkhsh(d):
    src = (SRC / "conversion" / "base.py").read_text()
    chktxt = ast.literal_eval(re.search(r"\n        chktxt = (.*)\n", src).group(1))
    from transformers import AutoTokenizer
    tok = AutoTokenizer.from_pretrained(d)
    h = hashlib.sha256(str(tok.encode(chktxt)).encode()).hexdigest()
    names = re.findall(r'if chkhsh == "([0-9a-f]{64})":\n\s+# ref: (\S+)\n\s+res = "([^"]+)"', src)
    hit = [n for n in names if n[0] == h]
    t = json.load(open(Path(d) / "tokenizer.json"))
    m = t["model"]
    inv = {v: k for k, v in m["vocab"].items()}
    for a in t.get("added_tokens", []):
        inv[a["id"]] = a["content"]
    n = json.load(open(Path(d) / "config.json")).get("vocab_size", 32000)
    vm = hashlib.sha256()
    for i in range(n):
        vm.update((inv.get(i, f"[PAD{i}]")).encode() + b"\n")
    vm.update(b"\n")
    for x in m["merges"]:
        x = x if isinstance(x, str) else " ".join("".join(chr(ord(c) + 256) if c == " " else c for c in p) for p in x)
        vm.update((x + "\n").encode())
    print(json.dumps({"dir": d, "chkhsh": h, "llama.cpp pre": hit[0][2] if hit else None, "ref": hit[0][1] if hit else None,
                      "vocab_merges_sha256": vm.hexdigest(), "pre_tokenizer": t.get("pre_tokenizer")}, indent=1))


NAMES = """smollm2-135m-instruct mindx-gen39 Mixtral-8x7B-Instruct-v0.1 meta-llama/Meta-Llama-3-8B bigscience/bloom-7b1-petals
Qwen/Qwen2.5-0.5B-Instruct ./my-model ../x/model-7B HuggingFaceTB/SmolLM2-135M-Instruct HuggingFaceTB/SmolLM-135M
prism-ml/Bonsai-8B-gguf Qwen/Qwen3-8B Qwen/Qwen3-0.6B-Base microsoft/Phi-3-mini-4k-instruct google/gemma-2-2b-it
mistralai/Mistral-7B-Instruct-v0.3 deepseek-ai/DeepSeek-V2-Lite-Chat tiiuae/falcon-7b EleutherAI/pythia-1.4b-deduped
TheBloke/Llama-2-13B-chat-GPTQ stabilityai/stablelm-2-1_6b openai-community/gpt2-xl allenai/OLMo-7B-0724-Instruct-hf
ibm-granite/granite-3.0-2b-instruct unsloth/Llama-3.2-1B-Instruct mistralai/Mixtral-8x22B-v0.1 Qwen/Qwen1.5-MoE-A2.7B
model-32k-chat merged checkpoint gen39 mindx-gen40-v2 mindX-Gen41-135M-lora Llama-3-70B-Instruct-AWQ q4_k_m-model
some-model-F16 Model-bf16-v1.2.3 tiny-llama-1.1b-chat-v1.0 x-small-mini-medium iter12-model A3B-thing model-7b1""".split()


def names(out):
    g = gguf_mod()
    from gguf.metadata import Metadata
    with open(out, "w") as f:
        for i in NAMES:
            for total in (0, 134_515_008, 8_190_000_000, -2_000_000):
                parts = Metadata.get_model_id_components(i, total)
                f.write(json.dumps({"id": i, "total": total, "parts": list(parts), "title": Metadata.id_to_title(i),
                                    "size": g.size_label(abs(total), abs(total), 0, 0) if total else None}) + "\n")
    print(f"{len(NAMES) * 4} cases → {out}")


if __name__ == "__main__":
    a = sys.argv[1:]
    if len(a) == 3 and a[0] == "--record":
        record(a[1], a[2])
    elif len(a) == 3 and a[0] == "--bankml":
        bankml(a[1], a[2])
    elif len(a) == 3 and a[0] == "--compare":
        compare(a[1], a[2])
    elif len(a) == 2 and a[0] == "--chkhsh":
        chkhsh(a[1])
    elif len(a) == 2 and a[0] == "--names":
        names(a[1])
    else:
        sys.exit(__doc__)