File size: 8,694 Bytes
acd280c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
198
199
200
201
202
203
204
205
206
207
"""权重忠实性验证(长期留存,比 _cmp_* 一次性诊断更值得留)。

为什么需要它
------------
本项目用的两个嵌入模型里,`pubmedbert` 目录下的 `model.safetensors` 与
`tokenizer.json` **是本地生成的**(上游只给 `pytorch_model.bin` + `vocab.txt`)。
而 PROGRESS.md 坑 7 记录过一次"大小完全正确、内容全是坏的"的静默损坏事故。

所以下面这个失败模式必须能排除:
    "PubMedBERT 上 semantic F1 地板高到 0.95" —— 是真模型性质,还是权重坏了?

办法:从上游拿原始 `pytorch_model.bin`(ModelScope,带官方 sha256),逐张量比对。

本脚本对每个模型做三件事:
  1. 本地文件 sha256 vs ModelScope 声明的 sha256(上游真有的文件);本地生成的文件标注清楚
  2. 本地 safetensors vs 上游 pytorch_model.bin 的**逐键 bitwise** 比对
  3. 两条加载路径的**前向输出**是否逐位一致

跑法:
    python scripts/00d_verify_embedder_weights.py                 # 两个模型都验
    python scripts/00d_verify_embedder_weights.py --models pubmedbert
"""

from __future__ import annotations

import argparse
import hashlib
import json
import sys
import urllib.request
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
import rnaqwen  # noqa: E402,F401  —— env.shim

import torch  # noqa: E402
from transformers import AutoModel, AutoTokenizer  # noqa: E402

MS_API = "https://www.modelscope.cn/api/v1/models/{repo}/repo/files?Revision=master&Recursive=True"

# 模型的仓库名与本地目录名,以及**本地生成**(上游没有)的文件清单
MODELS = {
    "pubmedbert": {
        "dir": ROOT / "models" / "pubmedbert",
        "repo": "microsoft/BiomedNLP-BiomedBERT-base-uncased-abstract-fulltext",
        "locally_generated": ["model.safetensors", "tokenizer.json"],
        "upstream_bin": ROOT / "models" / "pubmedbert_upstream" / "pytorch_model.bin",
    },
    "bert": {
        "dir": ROOT / "models" / "bert-base-uncased",
        "repo": "google-bert/bert-base-uncased",
        "locally_generated": [],
        "upstream_bin": None,  # 上游自带 safetensors,无需比对 .bin
    },
}

TEXTS = [
    "The RNA molecule is a non-coding RNA that regulates gene expression.",
    "The protein adopts an eight-stranded beta-barrel fold.",
    "The central bank raised its policy rate by twenty-five basis points.",
    "Sear the lamb shoulder on all sides until the fat renders golden.",
]


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


def ms_manifest(repo: str) -> dict:
    """ModelScope 官方清单(含官方 sha256)。拿不到就返回空 —— 别让网络问题假装成权重问题。"""
    try:
        req = urllib.request.Request(MS_API.format(repo=repo), headers={"User-Agent": "rnaqwen/0.1"})
        with urllib.request.urlopen(req, timeout=90) as r:
            payload = json.loads(r.read().decode())
    except Exception as e:  # noqa: BLE001
        print(f"    [warn] 拉不到 ModelScope 清单({type(e).__name__}: {e})—— 跳过 sha256 对照")
        return {}
    return {
        f["Path"]: {"sha256": f.get("Sha256") or "", "size": f.get("Size")}
        for f in (payload.get("Data") or {}).get("Files") or []
        if f.get("Type") != "tree"
    }


def file_check(name: str, cfg: dict) -> list[dict]:
    print(f"\n[1] 文件级 sha256 ({name})", flush=True)
    man = ms_manifest(cfg["repo"])
    rows = []
    for f in sorted(cfg["dir"].iterdir()):
        if not f.is_file():
            continue
        local = sha256(f)
        ref = man.get(f.name, {}).get("sha256", "")
        if f.name in cfg["locally_generated"]:
            verdict = "本地生成(上游无此文件)"
        elif not ref:
            verdict = "上游清单里没有这个文件名"
        elif ref.lower() == local:
            verdict = "OK  sha256 == ModelScope 声明值"
        else:
            verdict = f"MISMATCH  ms={ref[:24]}"
        rows.append({"file": f.name, "size": f.stat().st_size, "sha256": local, "verdict": verdict})
        print(f"    {f.stat().st_size:>12d} B  {f.name:<22s} {verdict}", flush=True)
    return rows


def tensor_and_forward_check(name: str, cfg: dict) -> dict:
    up = cfg["upstream_bin"]
    out: dict = {"upstream_bin": str(up) if up else None}
    if not up or not up.exists():
        print(f"\n[2/3] 跳过逐张量比对(无上游 .bin:{up})", flush=True)
        return out

    print(f"\n[2] 逐张量比对({name}:本地 safetensors vs 上游 .bin)", flush=True)
    from safetensors.torch import load_file

    sd_up = torch.load(up, map_location="cpu", weights_only=True)
    sd_lo = load_file(str(cfg["dir"] / "model.safetensors"))

    def norm(k: str) -> str:
        return k[5:] if k.startswith("bert.") else k

    HEAD = ("cls.", "pooler.")
    up_n = {norm(k): v for k, v in sd_up.items() if not k.startswith(HEAD)}
    lo_n = {norm(k): v for k, v in sd_lo.items()}
    common = sorted(set(up_n) & set(lo_n))
    diffs = {}
    shape_bad = []
    for k in common:
        a, b = up_n[k], lo_n[k]
        if a.shape != b.shape:
            shape_bad.append(k)
            continue
        diffs[k] = float((a.float() - b.float()).abs().max())
    exact = sum(1 for d in diffs.values() if d == 0.0)
    worst = sorted(diffs.items(), key=lambda kv: -kv[1])[:3]
    print(f"    上游 {len(sd_up)} 张量(含 MLM 头 {len(sd_up) - len(up_n)})→ 归一后共同键 {len(common)}", flush=True)
    print(f"    bitwise 相同: {exact}/{len(diffs)}   shape 不一致: {len(shape_bad)}", flush=True)
    print(f"    max|Δ| 最大: " + ", ".join(f"{k}={v:.2e}" for k, v in worst), flush=True)
    out.update({
        "tensors_upstream": len(sd_up), "tensors_local": len(sd_lo),
        "common_keys": len(common), "bitwise_equal": exact,
        "max_abs_diff": max(diffs.values()) if diffs else None,
        "all_bitwise_equal": exact == len(diffs) and not shape_bad,
    })

    print(f"\n[3] 前向比对({name})", flush=True)
    tok = AutoTokenizer.from_pretrained(str(cfg["dir"]))
    enc = tok(TEXTS, return_tensors="pt", padding=True, truncation=True, max_length=512)
    m_lo = AutoModel.from_pretrained(str(cfg["dir"])).eval()
    m_up = AutoModel.from_config(m_lo.config).eval()
    report = m_up.load_state_dict({norm(k): v for k, v in sd_up.items() if not k.startswith("cls.")}, strict=False)
    print(f"    上游 .bin -> model: missing={len(report.missing_keys)} unexpected={len(report.unexpected_keys)}", flush=True)
    # load_state_dict 是 in-place 的,但漏装任何键都会让下面的比对变成"随机 vs 训练"
    assert not report.missing_keys, f"有键没装上,比对无意义: {report.missing_keys[:5]}"
    with torch.no_grad():
        h_lo = m_lo(**enc).last_hidden_state
        h_up = m_up(**enc).last_hidden_state
    same = torch.equal(h_lo, h_up)
    maxd = float((h_lo - h_up).abs().max())
    print(f"    last_hidden_state 逐位相同: {same}   max|Δ| = {maxd:.3e}", flush=True)
    out.update({"forward_bitwise_equal": same, "forward_max_abs_diff": maxd})
    return out


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--models", default="bert,pubmedbert")
    ap.add_argument("--out", type=Path, default=ROOT / "reports" / "embedder_weight_verification.json")
    args = ap.parse_args()

    report = {"purpose": "排除'地板高/指标异常'由权重静默损坏导致", "models": {}}
    for key in [s.strip() for s in args.models.split(",") if s.strip()]:
        cfg = MODELS.get(key)
        if not cfg:
            print(f"[skip] 未知模型 {key}")
            continue
        print("=" * 78)
        print(f"模型 {key}  ->  {cfg['repo']}")
        print("=" * 78)
        report["models"][key] = {
            "dir": str(cfg["dir"]),
            "repo": cfg["repo"],
            "files": file_check(key, cfg),
            **tensor_and_forward_check(key, cfg),
        }

    args.out.parent.mkdir(parents=True, exist_ok=True)
    args.out.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
    print(f"\n[done] -> {args.out}")

    ok = all(
        m.get("all_bitwise_equal", True) and m.get("forward_bitwise_equal", True)
        for m in report["models"].values()
    )
    print("[OK] 权重忠实" if ok else "[FAIL] 有权重不一致,先别用这些分数")
    return 0 if ok else 1


if __name__ == "__main__":
    raise SystemExit(main())