RNAQwen / scripts /00d_verify_embedder_weights.py
RomanCohort's picture
add scripts
acd280c verified
Raw History Blame Contribute Delete
8.69 kB
"""权重忠实性验证(长期留存,比 _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())