#!/usr/bin/env python3 """合成迷你 GGUF 回环测试: 复刻真实 HauhauCS GGUF 的全部布局约定 (2D 数据 [out,in] + 元数据 [in,out]、v-head value-major 置换、范数增量 -1、ssm_a 存 -A), 验证 gguf_qwen35 加载器的映射/反量化/置换/变换正确,含数值断言(cos)。 量化类型用 Q8_0(gguf 库支持量化;Q4_K 反量化已由真实文件 426/426 逐张量验证)。 用法: python test_gguf_loader.py [--keep](--keep 保留临时文件供 inspect_gguf 查看) """ from __future__ import annotations import argparse import os import sys import tempfile import numpy as np import torch HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) N_EMBD = 2560 FFN = 9216 N_HEAD, N_HEAD_KV, HEAD_DIM = 16, 4, 256 L_KEY_HEADS, L_KEY_HD, L_V_HEADS, L_V_HD = 16, 128, 32, 128 CONV_K = 4 GGUF_TO_HF = np.array([(i % 16) * 2 + (i // 16) for i in range(32)], dtype=np.int64) def hf_to_gguf(x): return x[GGUF_TO_HF] def w2d(rng, out, inn): return rng.standard_normal((out, inn)).astype(np.float32) def add2d(w, name, data, qtype=None): from gguf.quants import quantize from gguf import GGML_QUANT_SIZES out, inn = int(data.shape[0]), int(data.shape[1]) if qtype is not None: data = quantize(np.ascontiguousarray(data), qtype) block, tsize = GGML_QUANT_SIZES[qtype] w.add_tensor(name, data, raw_shape=(out, inn * tsize // block), raw_dtype=qtype) else: w.add_tensor(name, data, raw_shape=(out, inn)) def add1d(w, name, data): w.add_tensor(name, data) def add_layer(w, rng, i: int, is_full: bool, expect: dict): base = f"blk.{i}" pref = f"language_model.layers.{i}." norm_in = w2d(rng, N_EMBD, 1).ravel() + 0.5 norm_out = w2d(rng, N_EMBD, 1).ravel() + 0.5 add1d(w, f"{base}.attn_norm.weight", norm_in) add1d(w, f"{base}.post_attention_norm.weight", norm_out) expect[f"{pref}input_layernorm.weight"] = norm_in - 1.0 expect[f"{pref}post_attention_layernorm.weight"] = norm_out - 1.0 gate = w2d(rng, FFN, N_EMBD) up = w2d(rng, FFN, N_EMBD) down = w2d(rng, N_EMBD, FFN) add2d(w, f"{base}.ffn_gate.weight", gate, qtype=Q8) add2d(w, f"{base}.ffn_up.weight", up, qtype=Q8) add2d(w, f"{base}.ffn_down.weight", down, qtype=Q8) expect[f"{pref}mlp.gate_proj.weight"] = gate expect[f"{pref}mlp.up_proj.weight"] = up expect[f"{pref}mlp.down_proj.weight"] = down if is_full: q = w2d(rng, 2 * N_HEAD * HEAD_DIM, N_EMBD) k = w2d(rng, N_HEAD_KV * HEAD_DIM, N_EMBD) v = w2d(rng, N_HEAD_KV * HEAD_DIM, N_EMBD) o = w2d(rng, N_EMBD, N_HEAD * HEAD_DIM) qn = w2d(rng, HEAD_DIM, 1).ravel() + 0.5 kn = w2d(rng, HEAD_DIM, 1).ravel() + 0.5 add2d(w, f"{base}.attn_q.weight", q, qtype=Q8) add2d(w, f"{base}.attn_k.weight", k, qtype=Q8) add2d(w, f"{base}.attn_v.weight", v, qtype=Q8) add2d(w, f"{base}.attn_output.weight", o, qtype=Q8) add1d(w, f"{base}.attn_q_norm.weight", qn) add1d(w, f"{base}.attn_k_norm.weight", kn) expect[f"{pref}self_attn.q_proj.weight"] = q expect[f"{pref}self_attn.k_proj.weight"] = k expect[f"{pref}self_attn.v_proj.weight"] = v expect[f"{pref}self_attn.o_proj.weight"] = o expect[f"{pref}self_attn.q_norm.weight"] = qn - 1.0 expect[f"{pref}self_attn.k_norm.weight"] = kn - 1.0 else: k_dim, v_dim = L_KEY_HEADS * L_KEY_HD, L_V_HEADS * L_V_HD conv_dim = k_dim * 2 + v_dim qkv = w2d(rng, conv_dim, N_EMBD) qkv_hf = qkv.copy() qkv[v_dim:] = hf_to_gguf(qkv[v_dim:].reshape(32, 128, -1)).reshape(v_dim, -1) z = w2d(rng, v_dim, N_EMBD) z_hf = z.copy() z = hf_to_gguf(z.reshape(32, 128, -1)).reshape(v_dim, -1) conv = w2d(rng, conv_dim, CONV_K) conv_hf = conv.copy() conv[v_dim:] = hf_to_gguf(conv[v_dim:].reshape(32, 128, -1)).reshape(v_dim, -1) dt_hf = rng.standard_normal(L_V_HEADS).astype(np.float32) dt = hf_to_gguf(dt_hf) a_log = rng.standard_normal(L_V_HEADS).astype(np.float32) beta_hf = w2d(rng, L_V_HEADS, N_EMBD) beta = hf_to_gguf(beta_hf) alpha_hf = w2d(rng, L_V_HEADS, N_EMBD) alpha = hf_to_gguf(alpha_hf) s_norm = w2d(rng, L_V_HD, 1).ravel() + 0.5 out_proj = w2d(rng, N_EMBD, v_dim) out_proj_gguf = out_proj.reshape(N_EMBD, 32, 128)[:, GGUF_TO_HF, :].reshape(N_EMBD, v_dim) add2d(w, f"{base}.attn_qkv.weight", qkv, qtype=Q8) add2d(w, f"{base}.attn_gate.weight", z, qtype=Q8) w.add_tensor(f"{base}.ssm_conv1d.weight", conv, raw_shape=(conv_dim, CONV_K)) add1d(w, f"{base}.ssm_dt.bias", dt) add1d(w, f"{base}.ssm_a", hf_to_gguf(-np.exp(a_log)).astype(np.float32)) add2d(w, f"{base}.ssm_beta.weight", beta, qtype=Q8) add2d(w, f"{base}.ssm_alpha.weight", alpha, qtype=Q8) add1d(w, f"{base}.ssm_norm.weight", s_norm) add2d(w, f"{base}.ssm_out.weight", out_proj_gguf, qtype=Q8) expect[f"{pref}linear_attn.in_proj_qkv.weight"] = qkv_hf expect[f"{pref}linear_attn.in_proj_z.weight"] = z_hf expect[f"{pref}linear_attn.conv1d.weight"] = conv_hf.reshape(conv_dim, 1, CONV_K) expect[f"{pref}linear_attn.dt_bias"] = dt_hf expect[f"{pref}linear_attn.A_log"] = a_log expect[f"{pref}linear_attn.in_proj_b.weight"] = beta_hf expect[f"{pref}linear_attn.in_proj_a.weight"] = alpha_hf expect[f"{pref}linear_attn.norm.weight"] = s_norm expect[f"{pref}linear_attn.out_proj.weight"] = out_proj def main() -> None: global Q8 from gguf import GGUFWriter, GGMLQuantizationType Q8 = GGMLQuantizationType.Q8_0 ap = argparse.ArgumentParser() ap.add_argument("--keep", action="store_true") args = ap.parse_args() tmpdir = tempfile.mkdtemp(prefix="gguf_test_") path = os.path.join(tmpdir, "toy_qwen35.gguf") print(f"写入合成 GGUF -> {path}") rng = np.random.default_rng(42) expect: dict[str, np.ndarray] = {} w = GGUFWriter(path, "qwen35") emb = w2d(rng, 248320, N_EMBD) add2d(w, "token_embd.weight", emb) expect["language_model.embed_tokens.weight"] = emb out_norm = w2d(rng, N_EMBD, 1).ravel() + 0.5 add1d(w, "output_norm.weight", out_norm) expect["language_model.norm.weight"] = out_norm - 1.0 add_layer(w, rng, 0, is_full=False, expect=expect) add_layer(w, rng, 3, is_full=True, expect=expect) w.write_header_to_file() w.write_kv_data_to_file() w.write_tensors_to_file() w.close() print(f"GGUF 写入完成 ({os.path.getsize(path) / 1e6:.0f} MB)") from gguf_qwen35 import load_gguf_state_dict, build_hf_model sd = load_gguf_state_dict(path) print(f"映射出 {len(sd)} 个键") fails = 0 for key, want in sorted(expect.items()): if key not in sd: print(f" [FAIL] {key} MISSING") fails += 1 continue got = sd[key] ws = torch.from_numpy(want).float() gs = got.float() if tuple(gs.shape) != tuple(ws.shape): print(f" [FAIL] {key} shape {tuple(gs.shape)} 期望 {tuple(ws.shape)}") fails += 1 continue dot = torch.zeros((), dtype=torch.float64) na = torch.zeros((), dtype=torch.float64) nb = torch.zeros((), dtype=torch.float64) CH = 1 << 22 ga, wf = gs.flatten(), ws.flatten() for i in range(0, ga.numel(), CH): x, y = ga[i:i + CH], wf[i:i + CH] dot += (x * y).sum().double() na += (x * x).sum().double() nb += (y * y).sum().double() c = float(dot / (na.sqrt() * nb.sqrt() + 1e-30)) ok = c > 0.99 print(f" [{'PASS' if ok else 'FAIL'}] {key} {tuple(gs.shape)} cos={c:.5f}") fails += 0 if ok else 1 model, missing, unexpected = build_hf_model(sd, os.path.join(HERE, "qwen35_config.json")) unexp_ok = len(unexpected) == 0 print(f" [{'PASS' if unexp_ok else 'FAIL'}] load_state_dict unexpected={len(unexpected)} (应=0)" + (f": {unexpected[:4]}" if unexpected else "")) fails += 0 if unexp_ok else 1 print(f" [info] missing={len(missing)}(其余 30 层 + vision 属正常缺省)") if not args.keep: import shutil shutil.rmtree(tmpdir) print("GGUF 回环", "PASS ✅" if fails == 0 else f"FAIL ({fails})") raise SystemExit(0 if fails == 0 else 1) if __name__ == "__main__": main()