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
Download scripts/00d_fetch_verified.py from RomanCohort/RNAQwen: direct link, hf CLI and curl.
- Browser
- Download file 13.8 kB
-
https://huggingface.co/RomanCohort/RNAQwen/resolve/main/scripts/00d_fetch_verified.py
- Command line
-
hf download hf://RomanCohort/RNAQwen/scripts/00d_fetch_verified.py
-
curl -L -o 00d_fetch_verified.py https://huggingface.co/RomanCohort/RNAQwen/resolve/main/scripts/00d_fetch_verified.py
13.8 kB
| """通用「带哈希闸门」的模型取数。 | |
| 为什么不用 `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()) | |