File size: 11,131 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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | #!/usr/bin/env python3
# SPDX-License-Identifier: MIT OR Apache-2.0
"""P3's whole-model oracle, from the SHIPPED ggml (llama.cpp b11192): every layer of the Qwen3 graph, output_norm and
the logits, built through ctypes with the ops llama.cpp's graph uses and computed by the release's own CPU backend
(libggml-cpu-haswell.so, 3 threads), for a chat prompt of fewer than 64 tokens (so attention takes ggml's reference
flash-attention path, as testing/forward_oracle.py's layer-0 checks do).
The graph is computed one layer at a time, each in its own context, carrying the residual stream between them as
f32 bytes, so memory stays near one layer's weights; the arithmetic is the same as one graph's.
Runs Q1_0 (1-bit) and Q2_0_g64 (ternary) models; since 0.3.4 also tied embeddings (no output.weight: the logits
from token_embd, as llama.cpp duplicates it), F16 weights and the Llama graph (no Q/K norms, RoPE in NORM mode, no
YaRN: ext_factor 0, attn_factor 1, as llama-context.cpp derives them without rope scaling). Writes .models/oracle-forward/model-<gguf stem>.tsv: a header line (the tokens), then per token and layer the sha256 of l_out's
f32 row, per token the sha256 of result_norm and of the logits row, the argmax, and the top five ids with their
logits (for eyes).
usage: python3 testing/model_oracle.py GGUF LIBDIR [OUT]"""
import ctypes as C, hashlib, mmap, os, struct, sys
from pathlib import Path
gguf, libdir = sys.argv[1], sys.argv[2]
out = Path(sys.argv[3] if len(sys.argv) > 3 else Path(__file__).resolve().parents[1] / ".models" / "oracle-forward")
out.mkdir(parents=True, exist_ok=True)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from gguf_guard import parse # noqa: E402
base = C.CDLL(os.path.join(libdir, "libggml-base.so"), mode=C.RTLD_GLOBAL)
cpu = C.CDLL(os.path.join(libdir, "libggml-cpu-haswell.so"), mode=C.RTLD_GLOBAL)
cpu.ggml_cpu_init()
class InitParams(C.Structure):
_fields_ = [("mem_size", C.c_size_t), ("mem_buffer", C.c_void_p), ("no_alloc", C.c_bool)]
P = C.c_void_p
base.ggml_init.restype = P
base.ggml_init.argtypes = [InitParams]
for f, args in (("ggml_mul_mat", [P, P, P]), ("ggml_new_tensor_1d", [P, C.c_int, C.c_int64]),
("ggml_new_tensor_2d", [P, C.c_int, C.c_int64, C.c_int64]), ("ggml_new_tensor_3d", [P, C.c_int, C.c_int64, C.c_int64, C.c_int64]),
("ggml_get_rows", [P, P, P]), ("ggml_rms_norm", [P, P, C.c_float]), ("ggml_mul", [P, P, P]), ("ggml_add", [P, P, P]),
("ggml_new_graph", [P]), ("ggml_get_data", [P]), ("ggml_reshape_2d", [P, P, C.c_int64, C.c_int64]),
("ggml_reshape_3d", [P, P, C.c_int64, C.c_int64, C.c_int64]), ("ggml_permute", [P, P, C.c_int, C.c_int, C.c_int, C.c_int]),
("ggml_cpy", [P, P, P]), ("ggml_swiglu_split", [P, P, P]),
("ggml_rope_ext", [P, P, P, P, C.c_int, C.c_int, C.c_int, C.c_float, C.c_float, C.c_float, C.c_float, C.c_float, C.c_float]),
("ggml_flash_attn_ext", [P, P, P, P, P, C.c_float, C.c_float, C.c_float])):
getattr(base, f).restype = P
getattr(base, f).argtypes = args
base.ggml_build_forward_expand.argtypes = [P, P]
base.ggml_free.argtypes = [P]
base.ggml_prec_set_acc.argtypes = [P, C.c_int]
cpu.ggml_graph_compute_with_ctx.argtypes = [P, P, C.c_int]
F32, F16, I32, Q1_0, Q2_0 = 0, 1, 26, 41, 42
QK = {Q1_0: 128, Q2_0: 64, F16: 1}
BLOCK = {Q1_0: 18, Q2_0: 18, F16: 2}
fh = open(gguf, "rb")
mm = mmap.mmap(fh.fileno(), 0, access=mmap.ACCESS_READ)
ver, kv, tensors, align, data_start = parse(mm[:64 << 20])
T = {t["name"]: t for t in tensors}
arch = kv["general.architecture"]
assert arch in ("qwen3", "llama"), arch
A = lambda k, d=None: kv.get(f"{arch}.{k}", d) # noqa: E731
eps = float(A("attention.layer_norm_rms_epsilon"))
n_layer = int(A("block_count"))
n_embd = int(A("embedding_length"))
n_ff = int(A("feed_forward_length"))
n_head, n_head_kv = int(A("attention.head_count")), int(A("attention.head_count_kv"))
head = int(A("attention.key_length", n_embd // n_head))
n_vocab = T["token_embd.weight"]["dims"][1]
OUTPUT = "output.weight" if "output.weight" in T else "token_embd.weight" # tied embeddings: llama.cpp duplicates token_embd
ROPE_MODE = 2 if arch == "qwen3" else 0 # NEOX for Qwen3, NORM for Llama (llama_model_rope_type)
libm = C.CDLL("libm.so.6")
libm.logf.restype = C.c_float
libm.logf.argtypes = [C.c_float]
f32 = lambda x: struct.unpack("<f", struct.pack("<f", x))[0] # noqa: E731
yarn = A("rope.scaling.type") == "yarn"
factor = f32(float(A("rope.scaling.factor", 1.0))) if yarn else 1.0
freq_scale = f32(1.0 / factor)
logfac = libm.logf(f32(1.0 / freq_scale))
mscale = f32(f32(f32(0.1) * logfac) + 1.0) if factor > 1.0 else 1.0
attn_factor = f32(mscale * f32(1.0 / f32(1.0 + f32(0.1 * logfac)))) if yarn else 1.0
ext_factor = 1.0 if yarn else 0.0
n_ctx_orig = int(A("rope.scaling.original_context_length", A("context_length")))
freq_base = float(A("rope.freq_base", 10000.0))
kq_scale = f32(1.0 / f32(head ** 0.5))
if n_vocab > 150000: # the Qwen3 vocabulary: "You are Savante." / "What does a bonsai need?" in the Bonsai template
prompt = [151644, 8948, 198, 2610, 525, 20739, 5409, 13, 151645, 198, 151644, 872, 198, 3838, 1558, 264, 22725, 12118, 30, 151645, 198,
151644, 77091, 198, 151667, 271, 151668, 271]
else: # the SmolLM2 vocabulary, the same conversation in ChatML (llama-server b11192's /apply-template + /tokenize)
prompt = [1, 9690, 198, 2683, 359, 12404, 14648, 30, 2, 198, 1, 4093, 198, 1780, 1072, 253, 48219, 737, 47, 2, 198, 1, 520, 9531, 198]
n = len(prompt)
assert n < 64, "the reference attention path needs fewer than 64 query rows"
def q1_bytes(t): # a matrix's bytes: 18-byte blocks of 128 (Q1_0) or 64 (Q2_0_g64) weights, or 2 bytes per F16 weight
return t["dims"][1] * (t["dims"][0] // QK[t["type"]]) * BLOCK[t["type"]]
def load(ctx, name):
t = T[name]
if t["type"] == F32:
x = base.ggml_new_tensor_1d(ctx, F32, t["dims"][0])
nb = 4 * t["dims"][0]
else:
assert t["type"] in QK, (name, t["type"])
x = base.ggml_new_tensor_2d(ctx, t["type"], t["dims"][0], t["dims"][1])
nb = q1_bytes(t)
C.memmove(base.ggml_get_data(x), C.c_char_p(mm[data_start + t["offset"]: data_start + t["offset"] + nb]), nb)
return x
def rows(t, width):
raw = C.string_at(base.ggml_get_data(t), width * 4 * n)
return [raw[p * width * 4:(p + 1) * width * 4] for p in range(n)]
def new_ctx(weight_bytes):
return base.ggml_init(InitParams(weight_bytes + (192 << 20), None, False))
def compute(ctx, t):
gf = base.ggml_new_graph(ctx)
base.ggml_build_forward_expand(gf, t)
cpu.ggml_graph_compute_with_ctx(ctx, gf, 3)
# the embedding
ctx = new_ctx(q1_bytes(T["token_embd.weight"]))
tok = base.ggml_new_tensor_1d(ctx, I32, n)
C.memmove(base.ggml_get_data(tok), (C.c_int32 * n)(*prompt), 4 * n)
x = base.ggml_get_rows(ctx, load(ctx, "token_embd.weight"), tok)
compute(ctx, x)
stream = C.string_at(base.ggml_get_data(x), n_embd * 4 * n)
base.ggml_free(ctx)
lines = []
mask_vals = (C.c_uint16 * (n * n))(*[0 if j <= i else 0xFC00 for i in range(n) for j in range(n)])
for il in range(n_layer):
names = [f"blk.{il}.{w}.weight" for w in ("attn_norm", "attn_q", "attn_k", "attn_v", "attn_q_norm", "attn_k_norm", "attn_output",
"ffn_norm", "ffn_gate", "ffn_up", "ffn_down") if f"blk.{il}.{w}.weight" in T]
ctx = new_ctx(sum(q1_bytes(T[m]) if T[m]["type"] in QK else 4 * T[m]["dims"][0] for m in names))
W = {m.split(".")[2]: load(ctx, m) for m in names}
inp = base.ggml_new_tensor_2d(ctx, F32, n_embd, n)
C.memmove(base.ggml_get_data(inp), C.c_char_p(stream), len(stream))
pos = base.ggml_new_tensor_1d(ctx, I32, n)
C.memmove(base.ggml_get_data(pos), (C.c_int32 * n)(*range(n)), 4 * n)
mask = base.ggml_new_tensor_2d(ctx, F16, n, n)
C.memmove(base.ggml_get_data(mask), mask_vals, 2 * n * n)
xn = base.ggml_mul(ctx, base.ggml_rms_norm(ctx, inp, C.c_float(eps)), W["attn_norm"])
def qk(wname, nname, nh):
r = base.ggml_reshape_3d(ctx, base.ggml_mul_mat(ctx, W[wname], xn), head, nh, n)
if nname in W: # Qwen3's per-head norms; Llama has none
r = base.ggml_mul(ctx, base.ggml_rms_norm(ctx, r, C.c_float(eps)), W[nname])
return base.ggml_rope_ext(ctx, r, pos, None, head, ROPE_MODE, n_ctx_orig, C.c_float(freq_base), C.c_float(freq_scale),
C.c_float(ext_factor), C.c_float(attn_factor), C.c_float(32.0), C.c_float(1.0))
q = qk("attn_q", "attn_q_norm", n_head)
k = qk("attn_k", "attn_k_norm", n_head_kv)
v = base.ggml_reshape_3d(ctx, base.ggml_mul_mat(ctx, W["attn_v"], xn), head, n_head_kv, n)
k16 = base.ggml_cpy(ctx, k, base.ggml_new_tensor_3d(ctx, F16, head, n_head_kv, n))
v16 = base.ggml_cpy(ctx, v, base.ggml_new_tensor_3d(ctx, F16, head, n_head_kv, n))
fa = base.ggml_flash_attn_ext(ctx, base.ggml_permute(ctx, q, 0, 2, 1, 3), base.ggml_permute(ctx, k16, 0, 2, 1, 3),
base.ggml_permute(ctx, v16, 0, 2, 1, 3), mask, C.c_float(kq_scale), C.c_float(0.0), C.c_float(0.0))
base.ggml_prec_set_acc(fa, 10)
ffn_inp = base.ggml_add(ctx, base.ggml_mul_mat(ctx, W["attn_output"], base.ggml_reshape_2d(ctx, fa, n_head * head, n)), inp)
fn = base.ggml_mul(ctx, base.ggml_rms_norm(ctx, ffn_inp, C.c_float(eps)), W["ffn_norm"])
sw = base.ggml_swiglu_split(ctx, base.ggml_mul_mat(ctx, W["ffn_gate"], fn), base.ggml_mul_mat(ctx, W["ffn_up"], fn))
l_out = base.ggml_add(ctx, base.ggml_mul_mat(ctx, W["ffn_down"], sw), ffn_inp)
compute(ctx, l_out)
for p, r in enumerate(rows(l_out, n_embd)):
lines.append(f"l_out\t{il}\t{p}\t{hashlib.sha256(r).hexdigest()}\t{' '.join(f'{v:.6g}' for v in struct.unpack('<3f', r[:12]))}")
stream = C.string_at(base.ggml_get_data(l_out), n_embd * 4 * n)
base.ggml_free(ctx)
print(f"layer {il + 1}/{n_layer}", end="\r", flush=True)
# output_norm and the logits
ctx = new_ctx(q1_bytes(T[OUTPUT]) + 4 * n_embd + 4 * n_vocab * n)
inp = base.ggml_new_tensor_2d(ctx, F32, n_embd, n)
C.memmove(base.ggml_get_data(inp), C.c_char_p(stream), len(stream))
rn = base.ggml_mul(ctx, base.ggml_rms_norm(ctx, inp, C.c_float(eps)), load(ctx, "output_norm.weight"))
logits = base.ggml_mul_mat(ctx, load(ctx, OUTPUT), rn)
compute(ctx, logits)
for p, r in enumerate(rows(rn, n_embd)):
lines.append(f"result_norm\t-\t{p}\t{hashlib.sha256(r).hexdigest()}\t{' '.join(f'{v:.6g}' for v in struct.unpack('<3f', r[:12]))}")
for p, r in enumerate(rows(logits, n_vocab)):
vals = struct.unpack(f"<{n_vocab}f", r)
top = sorted(range(n_vocab), key=lambda i: -vals[i])[:5]
lines.append(f"logits\t{top[0]}\t{p}\t{hashlib.sha256(r).hexdigest()}\t{' '.join(f'{i}:{vals[i]:.6g}' for i in top)}")
base.ggml_free(ctx)
dest = out / f"model-{Path(gguf).stem}.tsv"
with open(dest, "w") as f:
f.write(f"# {n_layer} layers · tokens {' '.join(map(str, prompt))}\n")
f.write("\n".join(lines) + "\n")
print(f"\n{n_layer} layers, result_norm and logits for {n} tokens → {dest}")
|