RNAQwen / scripts /00e_refetch_rnafm.py
RomanCohort's picture
add scripts
acd280c verified
Raw History Blame Contribute Delete
8.67 kB
"""重下 RNA-FM,硬校验 sha256。
为什么必须这么做(第二次栽同一个坑):
models/rnafm/model.safetensors 大小 398,117,588 与上游**逐字节相同**,
但 sha256 不符,205 个张量里 **155 个全零** —— 整个 12 层 encoder 是空的。
`from_pretrained` 不报错,LOAD REPORT 只提 lm_head/ss_head/pooler,
于是模型静默地变成「只看位置、不看序列」。所有下游结果都是废的。
规矩(与 Qwen3-8B 那次一样):
1) 单写者,下到 .part;
2) **强制** sha256 比对上游声明值;
3) 通过后才原子改名;
4) 再独立验证一次「换序列输出会变」。少了第 4 步,哈希对了也可能用错文件。
"""
from __future__ import annotations
import hashlib
import os
import shutil
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
ROOT = Path(r"C:\qwen-api\RNAQwen")
DEST = ROOT / "models" / "rnafm" / "model.safetensors"
PART = DEST.with_suffix(".safetensors.part")
URL = "https://huggingface.co/multimolecule/rnafm/resolve/main/model.safetensors"
EXPECT_SHA = "ec69333eb894dbefb580e690b0cbe1cbae9845fefd2bc78da914cd14226a2828"
EXPECT_SIZE = 398117588
def sha256_of(path: Path, chunk: int = 8 * 1024 * 1024) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
while True:
b = f.read(chunk)
if not b:
break
h.update(b)
return h.hexdigest()
def archive_corrupt() -> None:
if not DEST.exists():
return
cur = sha256_of(DEST)
if cur == EXPECT_SHA:
print("[skip] 现有文件哈希已正确,无需重下")
return
bad = DEST.with_name("model.safetensors.corrupt")
if bad.exists():
bad.unlink()
DEST.rename(bad)
print(f"[keep] 损坏文件留档 -> {bad.name} (sha256={cur[:16]}...)")
def download_aria2() -> bool:
exe = shutil.which("aria2c")
if not exe:
print("[aria2] 未找到 aria2c")
return False
# 断点续传:--continue=true 会读 .part.aria2 控制文件接着下。
# 之所以敢续,是因为下面 --file-allocation=none —— .part 里只有真实下到的数据,
# 不会有预分配留下的零填充空洞。**上次坏就坏在预分配 + 续传。**
# 哈希闸门在最后,续传期间拿到垃圾也无所谓,反正过不了 sha256。
cmd = [exe, "-x", "16", "-s", "16", "-k", "1M", "--continue=true",
"--file-allocation=none",
"--max-tries=0", "--retry-wait=10", "--timeout=60",
"--summary-interval=60", "--console-log-level=warn",
"-d", str(PART.parent), "-o", PART.name, URL]
print("[aria2] " + " ".join(cmd))
t0 = time.time()
rc = subprocess.call(cmd)
print(f"[aria2] 退出码 {rc},用时 {(time.time()-t0)/60:.1f} min")
if rc != 0:
print("[aria2] 非零退出。**别删 .part** —— 重跑本脚本会接着下。")
return PART.exists() and PART.stat().st_size > 0
def download_urllib() -> bool:
"""回退:单连接。支持用 Range 续传(aria2 挂了的时候不至于前功尽弃)。"""
have = PART.stat().st_size if PART.exists() else 0
headers = {"User-Agent": "curl/8.0"}
if have:
headers["Range"] = f"bytes={have}-"
print(f"[urllib] 回退单连接下载,从 {have/1e6:.1f} MB 续传")
else:
print("[urllib] 回退单连接下载")
try:
# ⚠️ urllib 的 timeout 只吃标量。写 (30, 120) 是 requests 的习惯 -> TypeError。
req = urllib.request.Request(URL, headers=headers)
with urllib.request.urlopen(req, timeout=120) as r, open(PART, "ab" if have else "wb") as f:
total = int(r.headers.get("Content-Length") or 0) + have
got, t0, last = have, time.time(), 0.0
while True:
b = r.read(1024 * 1024)
if not b:
break
f.write(b)
got += len(b)
if time.time() - last > 15:
last = time.time()
el = max(time.time() - t0, 0.01)
print(f" {got/1e6:8.1f}/{total/1e6:.1f} MB "
f"{(got-have)/el/1e6:6.2f} MB/s "
f"eta {(total-got)/max((got-have)/el,1)/60:5.1f} min", flush=True)
except Exception as e:
print("[urllib] 失败:", e)
return PART.exists()
return PART.exists()
def main() -> int:
print("=" * 72)
print("重下 RNA-FM 并硬校验")
print(" 目标 :", DEST)
print(" 期望 : size=%d sha256=%s" % (EXPECT_SIZE, EXPECT_SHA))
print("=" * 72)
archive_corrupt()
if DEST.exists() and sha256_of(DEST) == EXPECT_SHA:
print("[done] 已经是好的,直接进验证")
return verify()
if PART.exists() and not PART.with_name(PART.name + ".aria2").exists():
# 没有 aria2 控制文件的 .part = 上一次是 urllib 下的,续不了,只能重来
print("[note] 发现无控制文件的 .part,无法续传,删掉重下")
PART.unlink()
ok = download_aria2() or download_urllib()
if not ok:
print("[fail] 下载没成功")
return 1
size = PART.stat().st_size
print(f"[verify] 大小 {size} (期望 {EXPECT_SIZE}) {'OK' if size == EXPECT_SIZE else '不符'}")
got = sha256_of(PART)
print(f"[verify] sha256 {got}")
if got != EXPECT_SHA:
print("[FAIL] 哈希不符 —— **绝不改名**")
# 关键分支:
# 大小已满 + 哈希错 = aria2 自认为下完了,但内容里混着零填充空洞
# (预分配 + 中断的经典产物)。这种情况**续传救不了** —— 控制文件会说
# "所有分片都完成了",于是每次续传都立刻返回同样的坏文件,卡死循环。
# 必须连控制文件一起清掉,下次从头下。
# 大小未满 = 真的只是没下完,保留 .part 让它续。
if size >= EXPECT_SIZE:
ctl = PART.with_name(PART.name + ".aria2")
print(f"[FAIL] 大小已满但内容错 -> 判定为「预分配空洞」,清掉 .part 与控制文件重来")
print(f" 清 {PART.name}")
PART.unlink(missing_ok=True)
if ctl.exists():
print(f" 清 {ctl.name}")
ctl.unlink()
print(" **重跑本脚本即可**(会从头下,不会再续进这个坏状态)")
else:
print(f"[FAIL] 只下了 {size/1e6:.1f} MB,未完成 -> 保留 .part,重跑本脚本会接着下")
return 1
print("[verify] 哈希一致")
if DEST.exists():
DEST.unlink()
PART.rename(DEST) # 同盘 rename 是原子的
print(f"[done] 原子改名 -> {DEST.name}")
return verify()
def verify() -> int:
"""独立验证:换序列,encoder 输出必须变。"""
print("\n" + "=" * 72)
print("独立验证:序列敏感性(哈希对了也可能装错文件)")
print("=" * 72)
sys.path.insert(0, str(ROOT / "src"))
import rnaqwen # noqa: F401
import torch
from multimolecule.models.rnafm import RnaFmModel, RnaTokenizer
MP = str(DEST.parent)
m = RnaFmModel.from_pretrained(MP).eval()
tok = RnaTokenizer.from_pretrained(MP)
w = m.state_dict()["embeddings.word_embeddings.weight"].float()
print(" word_embeddings absmax = %.6e nnz = %d/%d"
% (float(w.abs().max()), int((w.abs() > 1e-8).sum()), w.numel()))
import glob
from safetensors.torch import load_file
raw = {}
for f in glob.glob(os.path.join(MP, "*.safetensors")):
raw.update(load_file(f))
zeros = [k for k in raw if raw[k].numel() and float(raw[k].float().abs().max()) == 0.0]
print(" 全零张量 = %d / %d" % (len(zeros), len(raw)))
seqs = ["ACGUACGUACGUACGUACGU", "GGGGGGGGGGGGGGGGGGGG", "UUUUUUUUUUUUUUUUUUUU"]
enc = tok(seqs, return_tensors="pt", padding=True)
with torch.no_grad():
o = m(**enc).last_hidden_state
d1 = float((o[0] - o[1]).abs().max())
d2 = float((o[0] - o[2]).abs().max())
print(" ACGU vs GGGG max|Δ| = %.6e" % d1)
print(" ACGU vs UUUU max|Δ| = %.6e" % d2)
print(" 输出形状 =", tuple(o.shape))
ok = (not zeros) and d1 > 1e-6 and d2 > 1e-6 and float(w.abs().max()) > 1e-6
print("\n[VERDICT]", "PASS —— RNA-FM 已修好,可以重跑 Stage 1" if ok
else "FAIL —— 别急着训,先查清楚")
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())