Instructions to use RomanCohort/RNAQwen with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use RomanCohort/RNAQwen with Transformers:
# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("RomanCohort/RNAQwen", device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 13,840 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 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | """通用「带哈希闸门」的模型取数。
为什么不用 `hf_hub_download` / `snapshot_download`:
2026-10-09 事故 —— `models/rnafm/model.safetensors` 大小 398,117,588 与上游**逐字节
相同**,但 205 个张量里 155 个全零(aria2 预分配 + 中断留下的零填充空洞),
`from_pretrained` 一声不响。**唯一能挡住它的是 sha256。**
规矩(写死在这里,别在调用点上重复实现):
1) 下到 `.part`;
2) **强制** sha256 比对上游声明值,不符绝不改名;
3) 大小已满但哈希错 => 判定为「预分配空洞」,连 .part.aria2 一起清掉
(这种情况续传救不了:控制文件会说"分片都完成了",每次续传立刻返回同一个坏文件);
4) 通过后才原子改名。
跑法:
python scripts/00d_fetch_verified.py \
--repo multimolecule/ernierna --dest models/ernierna \
--sha256 02274cb7... --size 345071800
不给 --sha256 时**拒绝大文件下载**(防止又走回"下来就用"的老路)。
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import shutil
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
UA = {"User-Agent": "Mozilla/5.0"}
# 大文件之外要一起拉的小文件(缺了模型加载不起来)
SMALL = ["config.json", "tokenizer_config.json", "vocab.txt",
"special_tokens_map.json", "configuration.json"]
def sha256_of(path: Path, chunk: int = 8 << 20) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for b in iter(lambda: f.read(chunk), b""):
h.update(b)
return h.hexdigest()
def real_progress(path: Path, chunk: int = 1 << 20) -> tuple[int, int, int]:
"""数「有多少 MB 块真有数据」。
⚠️ **逻辑大小不能当进度。** aria2 多连接按偏移写文件,逻辑大小会跳到最高已写位置,
中间可能是零。实测一份 325 MB 的 .part 里 255/311 块是空洞,真实数据只有 56 MB。
这个函数才是进度。
返回 (有数据块数, 总块数, 空洞位置前若干)。
"""
have = tot = 0
holes: list[int] = []
with open(path, "rb") as f:
off = 0
while True:
b = f.read(chunk)
if not b:
break
tot += 1
if any(b):
have += 1
elif len(holes) < 20:
holes.append(off // chunk)
off += len(b)
return have, tot, len(holes)
def hf_declared(repo: str, fname: str, host: str = "huggingface.co") -> tuple[str | None, int | None]:
"""问 HF 要官方声明的 sha256 / size。"""
url = f"https://{host}/api/models/{repo}?blobs=true"
req = urllib.request.Request(url, headers={**UA, "Accept": "application/json"})
with urllib.request.urlopen(req, timeout=40) as r:
d = json.load(r)
for s in d.get("siblings", []):
if s["rfilename"] == fname:
lfs = s.get("lfs") or {}
return lfs.get("sha256"), lfs.get("size")
return None, None
def fetch_small(repo: str, fname: str, dest: Path, rev: str,
host: str = "huggingface.co") -> bool:
url = f"https://{host}/{repo}/resolve/{rev}/{fname}"
try:
req = urllib.request.Request(url, headers=UA)
with urllib.request.urlopen(req, timeout=60) as r:
data = r.read()
(dest / fname).write_bytes(data)
print(f" {fname:<28} {len(data):>8} B OK")
return True
except Exception as e:
print(f" {fname:<28} 跳过 ({type(e).__name__}: {str(e)[:50]})")
return False
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--repo", required=True, help="例如 multimolecule/ernierna")
ap.add_argument("--file", default="model.safetensors")
ap.add_argument("--dest", type=Path, required=True)
ap.add_argument("--sha256", default=None, help="上游声明的 sha256;不给就自己去问 HF")
ap.add_argument("--size", type=int, default=None)
ap.add_argument("--rev", default="main")
ap.add_argument("--host", default="huggingface.co",
help="下载主机。hf-mirror.com 是镜像;**哈希用的是同一份声明值**,"
"所以从哪个 host 下都不影响正确性,只影响速度。")
ap.add_argument("--api-host", default="huggingface.co",
help="查元数据用哪个 host(声明 sha256 从这里拿)")
ap.add_argument("--connections", type=int, default=8,
help="aria2 连接数。16 在这条链路上握手失败率太高,默认降到 8")
ap.add_argument("--method", default="aria2", choices=["aria2", "curl"],
help="aria2=多连接续传(快,靠控制文件记进度);"
"curl=单连接续传(慢,但写出来的一定是连续前缀)")
ap.add_argument("--max-attempts", type=int, default=20,
help="失败重试次数。**重试不会删 .part**,所以进度不丢")
ap.add_argument("--no-verify", action="store_true",
help="**危险**:跳过 sha256。只有元数据接口拿不到哈希时才用,且要写进 PROGRESS")
args = ap.parse_args()
dest: Path = args.dest if args.dest.is_absolute() else (ROOT / args.dest)
dest.mkdir(parents=True, exist_ok=True)
big = dest / args.file
part = dest / (args.file + ".part")
url = f"https://{args.host}/{args.repo}/resolve/{args.rev}/{args.file}"
# ---- 单写者锁 ----
# 2026-10-09 真踩了:杀 aria2 **没杀它的父 python**,那个 python 的重试循环
# 立刻又拉起一个 aria2;与此同时我又起了第二个驱动器。
# 结果两个 aria2 写同一个 .part / .part.aria2。
# (数据本身没事 —— 两边写的是同样的字节、同样的偏移;但控制文件会互相踩,
# 进度读数变得不可信。)这个锁就是为这件事加的。
lock = dest / ".fetch.lock"
if lock.exists():
try:
old = int(lock.read_text(encoding="utf-8").split()[0])
except Exception:
old = -1
alive = False
if old > 0:
alive = subprocess.run(["tasklist", "/FI", f"PID eq {old}"],
capture_output=True, text=True).stdout.find(str(old)) >= 0
if alive:
print(f"[STOP] {dest} 已经被 PID {old} 的驱动器占着(单写者锁)。")
print(" 两个 aria2 写同一个 .part 会把控制文件搞乱。")
print(" 要么等它结束,要么先杀掉**父 python 进程**再重来。")
return 3
print(f"[note] 发现过期锁(PID {old} 已不在),接管")
lock.write_text(f"{os.getpid()} {time.strftime('%Y-%m-%d %H:%M:%S')}", encoding="utf-8")
want_sha, want_size = args.sha256, args.size
if want_sha is None:
print(f"[meta] 问 {args.api_host} 要 {args.repo} 的声明值 ...")
try:
want_sha, api_size = hf_declared(args.repo, args.file, args.api_host)
want_size = want_size or api_size
except Exception as e:
print(f"[meta] 失败: {e}")
print("=" * 74)
print(f"repo : {args.repo}")
print(f"file : {args.file}")
print(f"host : {args.host} (元数据来自 {args.api_host})")
print(f"dest : {dest}")
print(f"期望 : size={want_size} sha256={want_sha}")
print("=" * 74)
if not want_sha and not args.no_verify:
print("[STOP] 拿不到上游 sha256,拒绝下载大文件。")
print(" 真要跳过请显式加 --no-verify,并把原因写进 PROGRESS.md。")
return 2
# 已经好了?
if big.exists():
h = sha256_of(big)
if want_sha and h == want_sha:
print(f"[skip] 已存在且哈希正确 ({h[:16]}...)")
for f in SMALL:
if not (dest / f).exists():
fetch_small(args.repo, f, dest, args.rev, args.host)
return 0
print(f"[note] 已存在的文件哈希不符 ({h[:16]}... != {want_sha[:16] if want_sha else '?'}...)")
bad = dest / (args.file + ".corrupt")
if bad.exists():
bad.unlink()
big.rename(bad)
print(f"[keep] 留档 -> {bad.name}")
# 下
exe = shutil.which("aria2c")
print(f"[down] {url}")
if args.method == "aria2":
if not exe:
print("[STOP] 没有 aria2c。用 --method curl 或先装 aria2。")
return 2
# ⚠️ 这里必须循环重试,而且**不能删 .part / .part.aria2**。
#
# 为什么(2026-10-09 实测):
# aria2 多连接是按偏移写文件的,`--file-allocation=none` 下逻辑大小会跳到
# "最高已写位置",中间全是零。实测一份 .part:
# 逻辑大小 325,391,316 B(看起来 94%),311 个 MB 块里 **255 个是空洞**,
# 真实数据只有 56 MB(16%)。
# 所以**逻辑大小完全不能当作进度**。
# 而 `.part.aria2` 控制文件准确记录了哪些分片已完成 —— 它才是进度的真相。
# 一次失败就删文件,等于把真进度也扔了(上一版就是这么干的)。
#
# 正确性最后由 sha256 兜底:只要它没通过,就绝不改名。
for attempt in range(1, args.max_attempts + 1):
cmd = [exe, "-x", str(args.connections), "-s", str(args.connections),
"-k", "1M",
"--continue=true", # 用控制文件续传
"--file-allocation=none", # 不预分配
"--disable-ipv6=true",
"--max-tries=0", "--retry-wait=10", "--timeout=60",
"--lowest-speed-limit=1K", # 死连接快点丢掉换新的
"--summary-interval=60", "--console-log-level=warn",
"-d", str(dest), "-o", part.name, url]
print(f"\n[aria2] 第 {attempt}/{args.max_attempts} 次 "
f"(.part 逻辑大小 {part.stat().st_size if part.exists() else 0:,} B "
f"—— 注意这不是进度)", flush=True)
print("[aria2] " + " ".join(cmd), flush=True)
t0 = time.time()
rc = subprocess.call(cmd)
el = time.time() - t0
size_now = part.stat().st_size if part.exists() else 0
print(f"[aria2] rc={rc} 用时 {el/60:.1f} min 逻辑大小 {size_now:,} B")
if part.exists():
have, tot, nh = real_progress(part)
print(f"[真进度] 有数据块 {have}/{tot} (约 {have} MB / "
f"{args.size//(1<<20) if args.size else '?'} MB),空洞 {nh}")
if rc == 0:
print("[aria2] 退出码 0")
break
print(f"[aria2] 非零退出 —— **保留 .part 与控制文件**,重试会接着下")
time.sleep(5)
else:
print(f"[warn] {args.max_attempts} 次都没成功。.part 还在,再跑本命令即可。")
else:
# 单连接 curl 续传。慢,但写出来的一定是**连续前缀**,没有空洞歧义。
if not shutil.which("curl"):
print("[STOP] 没有 curl。")
return 2
for attempt in range(1, args.max_attempts + 1):
print(f"\n[curl] 第 {attempt}/{args.max_attempts} 次续传 "
f"(已有 {part.stat().st_size if part.exists() else 0:,} B)", flush=True)
rc = subprocess.call(["curl", "-sS", "-L", "-C", "-",
"--max-time", "1800",
"--speed-limit", "1024", "--speed-time", "60",
"-A", "curl/8.0", "-o", str(part), url])
print(f"[curl] rc={rc} "
f"大小 {part.stat().st_size if part.exists() else 0:,} B")
if rc == 0:
break
time.sleep(5)
if not part.exists():
print("[FAIL] 没有 .part,下载没起来")
return 1
# 校验
size = part.stat().st_size
print()
print(f"[verify] 大小 {size:,} (期望 {want_size}) "
f"{'OK' if want_size and size == want_size else '不符' if want_size else '未声明'}")
got = sha256_of(part)
print(f"[verify] sha256 {got}")
if want_sha and got != want_sha:
print("[FAIL] 哈希不符 —— 绝不改名")
if want_size and size >= want_size:
ctl = part.with_name(part.name + ".aria2")
print("[FAIL] 大小已满但内容错 -> 判定为预分配空洞,清掉重来")
part.unlink(missing_ok=True)
if ctl.exists():
ctl.unlink()
print(" 重跑本命令即可(会从头下)")
else:
print(f"[FAIL] 只下了 {size/1e6:.1f} MB,未完成 -> 保留 .part,重跑会接着下")
return 1
print("[verify] 哈希一致")
if big.exists():
big.unlink()
part.rename(big) # 同盘 rename 原子
print(f"[done] -> {big}")
print()
print("[meta] 小文件")
for f in SMALL:
if not (dest / f).exists():
fetch_small(args.repo, f, dest, args.rev, args.host)
print()
print("下一步(**必须**):")
print(f" & $PY scripts\\01c_check_encoder_sanity.py --model {dest} \\")
print(f" --expect-sha256 {want_sha} --expect-size {want_size}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
|