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 PROGRESS.md from RomanCohort/RNAQwen: direct link, hf CLI and curl.
- Browser
- Download file 49.7 kB
-
https://huggingface.co/RomanCohort/RNAQwen/resolve/main/PROGRESS.md
- Command line
-
hf download hf://RomanCohort/RNAQwen/PROGRESS.md
-
curl -L -o PROGRESS.md https://huggingface.co/RomanCohort/RNAQwen/resolve/main/PROGRESS.md
复现日志
每条都记 做了什么 / 验收到什么程度 / 踩了什么坑。 只写有证据的部分;没验证的不许写成"完成"。
2026-10-08 · 环境勘察与启动
机器事实(实测,不是推断)
| 项 | 值 | 怎么测的 |
|---|---|---|
| GPU | AMD Radeon 8060S (gfx1151),99.7 GB 统一内存,20 CU | torch.cuda.get_device_properties(0) |
| torch | 2.12.0a0+rocm7.13.0a20260313 |
装于 C:\ana\envs\comfyui |
| torch.distributed | is_available() == False —— 构建里没编 |
import torch.distributed as dist |
| transformers | 5.9.0(装进本项目 venv) | pip install |
| 解释器 | **C:\ana\envs\comfyui\python.exe**(用户指定用 comfyui 环境) |
见下方"环境变更" |
环境变更记录(装进 comfyui 环境,便于回滚)
按用户要求,本项目直接用 comfyui 环境跑,不另起环境。为此装进 C:\ana\envs\comfyui:
transformers 4.57.6 -> 5.9.0 (multimolecule 的硬需求:
huggingface-hub 0.36.2 -> 1.33.0 它 import transformers.utils.output_capturing,
hf-xet 1.5.2 -> 1.7.0 4.57 里没有这个模块)
新增:peft 0.21.2 · accelerate 1.15.0 · sentence-transformers 6.1.0 · rouge-score 0.1.2 · nltk 3.10.3
.venv(在项目目录下,--system-site-packages 继承 comfyui 的 torch)保留为备用,
包集合与 comfyui 一致,不动 comfyui。若 ComfyUI 的自定义节点依赖 transformers 4.x,用 .venv 跑本项目。
坑 1:import multimolecule 在本机直接不可行
danling(MultiMolecule 的运行时依赖)在 import 阶段无条件引用
torch.distributed.distributed_c10d、torch.distributed.tensor、dist.ReduceOp,
而这个 torch 构建没有分布式后端。四级连锁失败:
ImportError: cannot import name 'dcp' from 'danling.runners.checkpoints.torch_distributed'
ModuleNotFoundError: No module named 'torch._C._distributed_c10d'
AttributeError: module 'torch.distributed' has no attribute 'ReduceOp'
ModuleNotFoundError: No module named 'torch.distributed.tensor.device_mesh'
解法:src/rnaqwen/env/shim.py —— 把缺失符号补成占位符,
并在 sys.meta_path 末尾挂一个 permissive finder(真模块优先,缺的才伪造)。
现在 from multimolecule.models.rnafm import RnaFmModel, RnaTokenizer 通过(transformers 5.9.0)。
坑 2:网络 —— HF 走得通但慢到不可用,EBI 完全不通
| 通道 | 实测 |
|---|---|
huggingface.co CDN(us.aws.cdn.hf.co) |
0.06 MB/s |
| hf-mirror.com | 0.06 MB/s(回源到 hf.co,没用) |
ModelScope(cdn-lfs-cn-1.modelscope.cn) |
11.2 MB/s |
| NCBI eutils | 通(esearch / efetch / elink 全 200) |
| rnacentral.org / ftp.ebi.ac.uk / europepmc.org | SSL EOF / ReadTimeout,全不通 |
推论:
- 权重一律走 ModelScope(
scripts/00b_download_modelscope.py,并发 + 断点续传)。 - 数据源从 RNAcentral 换成 NCBI nuccore + pubmed(差异已写进 README §5)。
- 另外:
huggingface_hub走 hf-mirror 会抛FileMetadataError: Distant resource does not seem to be on huggingface.co—— 镜像不给 HEAD 元数据头。别用镜像。
坑 3:urllib/requests 的下载超时用法不一样,且 CDN 会「滴水」
urlopen(req, timeout=(20, 120))抛TypeError: 'tuple' object cannot be interpreted as an integer—— 元组超时是requests的用法。四个下载任务全灭了一次。- 换回
requests之后,读超时给太长(30s)等于卡死没有出口: CDN 会让连接保持存活但几乎不吐字节,进程静默挂在 0.00 MB/s 上半小时。 最终解法是timeout=(15, 10)(连接 15s / 读 10s)+一个速率看门狗 (45 秒内增量 < 200 KB 就主动断开,带Range续传重试)。 - 另一个坑:**
Get-ChildItem读到的文件大小是缓存的,明明在下载却一直显示 旧值,害得我以为卡了三次。改用 Python 的os.path.getsize才看到真实速率 (5 个 shard 各 ~2 MB/s,合计 ~10.7 MB/s)。别用 PowerShell 看下载进度。**
坑 4:磁盘写满(Qwen3-8B 下到一半全灭)
OSError [Errno 28] No space left on device —— 5 个 shard 同时卡在同一位置反复重试。
C 盘当时只剩 4.4 GB(共 918 GB,本来就快满了)。清理:
| 动作 | 释放 |
|---|---|
删掉多余的 conda clone C:\ana\envs\rnaqwen(我们有 .venv,用不到它) |
6.09 GB |
清掉 HF 缓存里的 .incomplete 残片 |
0.15 GB |
删掉 pubmedbert 里冗余的 pytorch_model.bin(已与 safetensors 逐位比对一致) |
0.44 GB |
清理后 11.16 GB 可用,15.3 GB 的权重才下得完。
教训:这台机器 C 盘长期紧张,下大模型前先看 Get-PSDrive C。
坑 5:HF 不是慢,是单连接被限速
aria2c -x16 -s16 -k1M 从 huggingface.co 拉 RNA-FM 的 398 MB:
25 秒下完(约 16 MB/s),而单连接只有 0.06 MB/s
所以真正的结论不是"HF 不能用",而是单连接被掐到 0.06 MB/s,多连接能跑满。
(ModelScope 单连接就能到 2–11 MB/s,但同样会在并发下载时互相挤。)
以后再遇到"某个源很慢",先试 -x16,别急着换源。
坑 6:下载并发多了会互相拖死
5 个下载任务 + 2 个 subagent 同时拉,结果是所有任务一起停(每个都显示 0.00 MB/s)。 最后按"一个源最多 5 条流、别再多开进程"来跑才稳定。
模型就位情况
| 模型 | 来源 | 状态 |
|---|---|---|
Qwen/Qwen3-8B |
ModelScope | 下载中(15.3 GB / 5 shard) |
multimolecule/rnafm |
HF(aria2 -x16) | 398 MB,即将完成 |
google-bert/bert-base-uncased |
ModelScope google-bert/bert-base-uncased |
✅ 441 MB,sha256 全对 |
microsoft/BiomedNLP-BiomedBERT-… |
ModelScope | ✅ 见下方说明 |
PubMedBERT 的一个事实:上游仓库(HF 和 ModelScope 一致)根本没有
model.safetensors,也没有 tokenizer.json —— 只有 pytorch_model.bin + vocab.txt。
所以本地这两个文件是生成的(safetensors 由 .bin 导出,tokenizer.json 由 vocab.txt 构造),
并已验证:safetensors 与 .bin 前向输出逐位一致(max|Δ| = 0.0)。
→ 报告里要写明这一点,别让人以为是从上游拿的原文件。
坑 7(最贵的一个):静默损坏 —— 大小完全正确,内容是坏的
Qwen3-8B 下完 16.38 GB,safetensors 加载成功(399/399 张量全部到位),
但生成出来是乱码:
prompt: "Name the four RNA nucleotides, separated by spaces."
sdpa / bfloat16 -> ',es numpy React sys React matplotlib React React React ...'
eager / bfloat16 -> ',es numpy React sys React matplotlib React React ...'
eager / float32 -> ',es matplotlib React React React React React React ...'
三种 attention 后端 × 两种精度输出同样的乱码 → 不是算子问题,是权重本身坏了。
scripts/00c_verify_models.py 拿本地 sha256 跟 ModelScope 声明的值对比:
model-00005-of-00005.safetensors 1244659840 1244659840 OK 20c2d636... == 20c2d636...
model-00001-of-00005.safetensors 3996250744 3996250744 BAD 8f63b006... != 31d6a825...
model-00002-of-00005.safetensors 3993160032 3993160032 BAD bd732b5f... != 5991236c...
model-00003-of-00005.safetensors 3959604768 3959604768 BAD 52d91d30... != c5185c47...
model-00004-of-00005.safetensors 3187841392 3187841392 BAD 41b73ee3... != b5ee7de7...
4/5 个 shard 坏了,而字节数一个不差。
成因(复盘):
- 这些文件被多轮续传拼过 —— 磁盘写满中断 → 换进程重下 → 再断 → 再续;
- 中途还出现过两个进程同时写同一个文件(先起了 26/27 号任务,又起了 29/30 号任务,旧任务没死透)。
Size 校验完全挡不住这种错误。修复:
00b_download_modelscope.py改成先写.part、完整拿到后原子改名;- 下完必须跑
00c_verify_models.py对 sha256 —— 不看 sha256 不算下完。
这条要写进开源仓库的 CONTRIBUTING:权重只做 size 校验是危险的, 静默损坏的模型会加载成功、跑得动、只是输出垃圾,排查成本极高。
坑 8:aria2c 会把文件预分配成完整大小
aria2 先按最终大小占位,进度只记在 .aria2 控制文件里。
我用 Get-ChildItem 看到 398.12 MB 就以为下完了,结果 safetensors 报
invalid JSON in header: EOF while parsing a value。
判断下载是否完成:看控制文件 / 校验和,不看文件大小。
型号核对:Qwen3-8B 的投影层确实零改动
已下到本地的 models/Qwen3-8B/config.json:
hidden_size = 4096 num_hidden_layers = 36
vocab_size = 151936 intermediate_size = 12288
与 Llama-3-8B 的 hidden=4096 逐位相同 → RNA-FM 那条 640 → 4096 线性投影
与原版维度一致,不用改。这就是选 Qwen3-8B(而不是 Qwen2.5-7B 的 3584)的理由。
注意区分:本机
C:\qwen-api\models\下的Qwen3.8-27B-*/Qwen3.6-35B-A3B-*是别的系列, 不能当底座。复现用的是原始 Qwen3 家族、8B 规模:Qwen/Qwen3-8B。
PLAN §7.3 · 评测脚本(已通过)
scripts/03_eval_selfcheck.py,报告 reports/eval_selfcheck_bert.json。
A 关:ROUGE 对数(自实现 vs Google rouge_score)
7 个用例:identical / paraphrase / partial / unrelated / empty / punctuation / numbers
-> 最大偏差 0.000e+00(阈值 1e-09):一致
逐样本完全一致。尺子准了。
B 关:语义指标不变量(嵌入 = bert-base-uncased)
[high] identical P=1.0000 R=1.0000 F1=1.0000
[high] paraphrase P=0.9396 R=0.9396 F1=0.9396
[mid ] partial P=0.9366 R=0.8748 F1=0.9046
[low ] unrelated P=0.8004 R=0.8321 F1=0.8159
[ok] identical_ge_0.999 / paraphrase_gt_partial / partial_gt_unrelated / unrelated_lt_0.95
B/C 关:PubMedBERT(reports/eval_selfcheck_pubmedbert.json)
A 关同 BERT(7 用例 Δmax = 0.000e+00)。B 关没过,C 关过了:
identical P=1.0000 R=1.0000 F1=1.0000
paraphrase P=0.9938 R=0.9938 F1=0.9938
partial P=0.9950 R=0.9857 F1=0.9903
unrelated P=0.9701 R=0.9669 F1=0.9685 <- 比 BERT 的 0.8159 还高
[ok ] identical_ge_0.999 / paraphrase_gt_partial / partial_gt_unrelated
[FAIL] unrelated_lt_0.95
这个 FAIL 不是 bug,是阈值本身不适用于 PubMedBERT。
unrelated_lt_0.95 是在 BERT 上标定的;而 PubMedBERT 的跨域地板是 0.9518
(§7.3b 实测),它在数学上不可能满足这条断言。
C 关同理可信度有限:
perfect_100pct 1.0000/1.0000/1.0000 F1=1.0000
half_50pct 0.4800/0.4583/0.4800 F1=0.9907
one_third 0.4800/0.4583/0.4800 F1=0.9907
bare_sequence_like 0.0000/0.0000/0.0000 F1=0.8889 <- 纯 ACGU 乱串也有 0.89
empty 0.0000/0.0000/0.0000 F1=0.0000
-> ROUGE-1 随答案完整度单调:是
ROUGE 阶梯正常(1.00 → 0.48 → 0.00); 但 semantic 那列「50% 答案 0.9907 / 乱码 0.8889」的区分度基本为零。
C 关:消融阶梯的响应方向
variant R-1 R-2 R-L F1@bert
perfect_100pct 1.0000 1.0000 1.0000 1.0000
half_50pct 0.4800 0.4583 0.4800 0.9370
one_third 0.4800 0.4583 0.4800 0.9370
bare_sequence_like 0.0000 0.0000 0.0000 0.2633
empty 0.0000 0.0000 0.0000 0.0000
-> ROUGE-1 随答案完整度单调:是
🔎 第一个发现(后来被部分推翻):语义 F1 的"地板很高"
「无关的两段文本」在句子级 max-cosine 下拿到 F1 = 0.8159。 当时据此推断分辨率被压缩在 **[0.82, 1.00]**。
⚠️ 这个推断后来被证伪了,见 §7.3b。 0.8159 之所以那么高,是因为 RNA 与蛋白质晶体学同属生物医学语域。换成真正的跨领域配对(RNA vs 经济学/法律/菜谱), 同一把尺子立刻掉到 0.45 左右。§7.3b 的结论比这里更严重,但方向不同: 地板在 BERT 上其实只有 0.52(不是 0.82),真正的病在 PubMedBERT(0.95)。 原文保留,记录判断的演进过程。
PLAN §7.3b · semantic F1 的分辨率地板(scripts/03b_semantic_floor.py)
报告:reports/semantic_floor.json(bert + pubmedbert 各一遍,n=2000,seed 20261008)。
先纠一个前提错误
原来把 unrelated(RNA vs 蛋白质晶体学)的 0.8159 当成"完全无关"的下限。
这是错的:两者同属生物医学语域、共享大量词根,句子向量本来就靠得近。
按领域两两配(RNA vs 经济学 / 法律 / 菜谱),立刻掉到 0.45 左右。
| 无关对(8句 vs 8句) | F1@bert | F1@pubmedbert |
|---|---|---|
| rna vs crystallography(都属生物医学) | 0.7663 | 0.9640 |
| rna vs economics | 0.4490 | 0.9592 |
| rna vs legal | 0.4649 | 0.9498 |
| rna vs cooking | 0.4549 | 0.9522 |
| crystallography vs legal | 0.4771 | 0.9426 |
| economics vs cooking | 0.5374 | 0.9585 |
→ 教训:**"无关"必须按语域设计对照,不能顺手拿两个技术领域当无关。** 语域相邻的"无关"会把地板抬高 0.3,足以把一个结论带反。
地板 / 随机基线 / 上界
| bert-base-uncased | pubmedbert | |
|---|---|---|
| 上界(identical) | 1.0000 | 1.0000 |
| 地板 A:跨域无关随机配对 | 0.5205 (std 0.089) | 0.9518 (std 0.008) |
| 地板 B:同域不同事实(分子生物学) | 0.7102 | 0.9682 |
| 地板 C:词序全打乱、词汇全对 | 0.8289 | 0.9718 |
| 地板 D:纯乱码 | 0.2483–0.3913 | 0.9170–0.9250 |
| 句级随机配对("瞎猜") | 0.5357 (std 0.147) | 0.9514 (std 0.015) |
| 同域句级随机配对 | 0.7236 | 0.9683 |
| 有效动态范围(跨域地板 → 1.0) | [0.52, 1.00],宽 0.479 | [0.95, 1.00],宽 0.048 |
最严重的一条:这条指标量的不是语义
把每个句子内部的词序全打乱 —— 句子彻底不成话,词 3-gram Jaccard 只剩 0.0035 (几乎没有保留任何连续词组),但:
bert-base-uncased 乱序 F1 = 0.8275 (原句 1.0000)
pubmedbert 乱序 F1 = 0.9679 (原句 1.0000)
PubMedBERT 上,"词汇全对但语义全错"(0.968)与"完全无关的领域"(0.952) 基本无法区分,和纯乱码(0.92)也差不了多少。 它对"领域/语域 + 词汇重叠"敏感,对"句子究竟说了什么"不敏感。
0.8609 在不同嵌入上的含义完全不同
| embedder | 地板 | 0.8609 的位置 |
|---|---|---|
| bert | 0.5205 | 高出地板 +0.3404,占可用窗口 71% —— 有信息量 |
| pubmedbert | 0.9518 | 低于地板 0.0909 —— 在这个尺度上等于"完全跑题" |
同一把尺子的同一个读数,在两种嵌入上一个算好、一个算跑题。
说明分母(地板)比分子更决定读数,也说明 S_BERT 与 S_Pub 的分数
不可横向比较 —— 原版表里 0.8609 vs 0.7165 的差距,很可能主要来自嵌入本身的性质,
而不是"BERT 语义更准"。
0.8609 是原论文在其自有评测集上的数,本地是自建小语料,绝对值不可直接比较; 这里只用它定位"同一把尺子在本地语料上的零点"。
机制:表示各向异性
| 跨域句级 cos | 同域句级 cos | 跨/同域差 | 各向异性 | |
|---|---|---|---|---|
| bert | 0.4844 | 0.7250 | +0.2406 | 0.7281 |
| pubmedbert | 0.9466 | 0.9684 | +0.0218 | 0.9752 |
PubMedBERT 的句向量挤在一个极窄的锥里(任意两句 cosine 就有 0.95), max-cosine 于是无论如何都被抬到 0.95 以上 —— 地板高是几何必然,不是评测集的问题。 它的同/跨域可分性比 BERT 低一个数量级(0.022 vs 0.240), 这才是它无法分辨对错的根本原因。
落在报告口径上的三条
- 不能单独用 semantic F1 判复现好坏。 必须同时报 ROUGE + 同语料的随机/无关基线。
S_BERT与S_Pub不可跨列比较(分母不同)。原版表把两种嵌入并排摆, 容易被读成"BERT 上语义更好",实际不是。- 报 PubMedBERT 的 semantic F1 时量级要特别小心 —— 0.95 以下没有信息,0.97 与 0.99 之间的差别基本是噪声。
PLAN §7.3c · PubMedBERT 权重忠实性验证(scripts/00d_verify_embedder_weights.py)
报告:reports/embedder_weight_verification.json。
为什么必须做:pubmedbert 目录下的 model.safetensors 与 tokenizer.json 是本地生成的
(上游只给 pytorch_model.bin + vocab.txt),而这里踩过一次
"字节数完全正确、内容全是坏的"的静默损坏(坑 7)。所以"PubMedBERT 地板 0.95"
必须先排除"权重坏了"才成立。
从 ModelScope 重下上游 pytorch_model.bin(440,474,434 B):
sha256 = ad7bbb66376cfd6b2db3447192b034efe016337cbef135c35c411fd61b13c193
== ModelScope 声明值(逐位相同)
上游 208 张量(含 MLM 头 9)→ 去 `bert.` 前缀、剔除 cls.* 后共同键 199
bitwise 相同: 199/199 shape 不一致: 0 max|Δ| = 0.000e+00
前向: last_hidden_state 逐位相同: True max|Δ| = 0.000e+00
句向量余弦矩阵 max|Δ| = 0.000e+00
[OK] 权重忠实
结论:本地 safetensors 是上游权重的忠实无损导出。地板 0.95 是真模型性质,不是损坏。
顺带修掉一个自己犯的错:第一版比对脚本写成
missing, unexpected = m_up.load_state_dict(sd_up, strict=False), 把返回值丢掉了,于是m_up还是随机初始化,前向 max|Δ| = 1.49e+01, 差点得出"权重坏了"这个完全相反的结论。load_state_dict确实是 in-place 的,但必须断言missing_keys为空才能拿去比前向 —— 现在脚本里有assert not report.missing_keys。
另一个易踩的坑:
model.safetensors是从AutoModel(裸 encoder)导出的,不含 MLM 头。 用AutoModelForMaskedLM加载会报cls.predictions.*MISSING,然后拿随机初始化的 MLM 头输出垃圾词(实测:The patient was diagnosed with [MASK] cancer.→vast/grand/looks, 而真 BERT 给的是breast/lung/liver)。 别用 MLM 探针验 encoder 权重 —— 会得出错误结论。semantic 指标只吃 encoder 输出,不受影响。
PLAN §7.1 · RNA-FM 探针(已通过)
reports/rnafm_probe.json(CPU 上跑的;GPU 另见下方"待查")。
config : max_position_embeddings=1026 hidden=640 vocab=28
buffer embeddings.position_ids: shape=(1, 1026)
[ok] demo_small nt= 81 ids=(1, 83) out=(1, 83, 640)
[ok] demo_yeast_trna_phe nt= 76 ids=(1, 78) out=(1, 78, 640)
[ctx] 1022 nt -> ids (1, 1024)
输出维度 640,逐位对上 config 的 hidden_size。 tokenizer 会额外加 BOS/EOS 两个特殊 token。
⚠️ 一个和 PLAN 假设不一致的实测结果:上下文其实是 1022 nt,不是 1024
PLAN §3 P0 的过滤标准写的是"序列长度 30–1024 nt(RNA-FM 上下文 1026 − 2)"。
scripts/01b_probe_rnafm_limit.py 逐长度逼边界,结果:
nt tokens result
1022 1024 OK out=(1, 1024, 640)
1023 1025 FAIL IndexError: index out of range in self
1024 1026 FAIL IndexError: index out of range in self
-> 可用上限:1022 nt
config.max_position_embeddings=1026 和 position_ids 缓冲区的 1026 都对得上,
但真正生效的 token 上限是 1024(1024 个 token 能过,1025 就炸)。
所以:数据过滤上限要从 1024 nt 收到 1022 nt。 (当前这 50 条最长 966 nt,没踩到;上量到 1K/121K 时必须改, 否则会有一批样本在 encoder 里直接抛 IndexError。)
GPU 上前向崩过一次 —— 是暂时的,已恢复
同一段探针 --device cuda 下崩在 at::native::to → empty_strided_cuda →
c10::cuda::SetDevice(HIP 侧抛异常,连 torch.zeros(8, device='cuda') 都过不去)。
CPU 上一切正常,说明不是权重问题。
排查过程:
- 先怀疑 GPU 被别的进程占着 ——
Get-Process显示没有大进程,可用内存 52 GB,排除; aria2c式的环境因素排除(两个解释器都崩);- 等了约 20 分钟后原样重试,直接通过(
CUDA OK),探针在 GPU 上也跑通了。
结论:gfx1151 + ROCm 7.13 nightly 偶发地会把设备卡住,重启内核态的驱动状态需要时间。
→ 遇到 c10_cuda_check_implementation / SetDevice 这类异常,
先重试 + 等一会儿,别急着改代码或重装 torch。
(reports/rnafm_probe_gpu.json 是 GPU 版结果,reports/rnafm_probe.json 是 CPU 版,内容一致。)
PLAN §7.2 · RNA-QA 重建(采集已通,生成待 GPU)
管线逻辑离线自检(已通过)
真跑要等 15.3 GB 的权重,但管线逻辑与模型无关。
scripts/99_selftest_pipeline.py 用一个"假生成器"把 11/12/13 的代码路径整条跑一遍,
假生成器故意做两件坏事:按频率吐出序列子串、按频率输出非 JSON 废话。
全部 8 项检查通过:
[1] divide(): 20/20 条切块干净 <- 块边界都落在句末
[2] 解析出 40 组 QA;非 JSON 输出 2 次;闸门拦下 4 条带序列的记录
[ok ] divide_keeps_sentence_boundaries
[ok ] json_parse_handles_fences <- ```json 围栏能剥
[ok ] guard_catches_leak <- 序列泄漏真的被拦
[ok ] guard_flags_bad_json
[ok ] guard_positive / guard_negative <- 闸门不误杀
[ok ] guard_ngram_detects_bare_bases <- 裸碱基串(非 reference 子串)也能抓
[ok ] wrote_jsonl
[verdict] PASS —— 管线逻辑没问题,等权重到位即可上量
意义:真跑之后如果数据是脏的,那一定是模型的问题,不是代码的问题 —— 归因干净。
已跑通:采集 50 条 (序列, 文献)
scripts/10_collect.py,最终链路 nuccore → GenBank → pubmed:
[batch 0] ids=60 gb=60 可用=60 pmids=18 已收 50/50
[stats] {"nt_scanned": 60, "bad_len": 0, "bad_alpha": 0, "no_pubmed": 0,
"short_abstract": 0, "no_abstract": 0, "dup": 0, "kept": 50}
[lens] min=116 median=416 p90=825 max=966 <- 全部落在 RNA-FM 的 1024 内
产物:data/raw/rna_literature_50.jsonl
两条被实测否掉的链路(写下来省得重走):
pubmed → nuccore(正向):拿到的全是 2–4 kb 完整 mRNA(NM_*), 越过 1024 nt 上限,半小时一条都收不到。elink反查:传多个 UID 时会合并成一个 linkset,不告诉你哪条链接属于哪条序列 (实测 5 个 id 的 pubmed 链接全挤在一起)。 → 改用 GenBank 自带的PUBMED字段,一次 efetch 同时拿序列和 PMID,一一对应。
Qwen3-8B 权重:已修复并校验通过
静默损坏的 1/2/3 号 shard 删掉重新单写者下载后:
model-00001-of-00005.safetensors 3996250744 3996250744 OK 31d6a825... == 31d6a825...
model-00002-of-00005.safetensors 3993160032 3993160032 OK 5991236c... == 5991236c...
model-00003-of-00005.safetensors 3959604768 3959604768 OK c5185c47... == c5185c47...
model-00004-of-00005.safetensors 3187841392 3187841392 OK b5ee7de7... == b5ee7de7...
model-00005-of-00005.safetensors 1244659840 1244659840 OK 20c2d636... == 20c2d636...
model.safetensors.index.json 32878 32878 OK f9fdbcb9... == f9fdbcb9...
[OK] 全部文件与 ModelScope 声明一致。
连贯性验证(之前是乱码的那次):
[gen] 载入 models/Qwen3-8B 用时 17.9s device=cuda dtype=torch.bfloat16
prompt: "Name the four RNA nucleotides, separated by spaces. Answer in one line."
OUT: 'Adenine Cytosine Guanine Uracil' <- 正常了
至此 PLAN §1 的两个模型(RNA-FM 640 维 + Qwen3-8B 4096 维)都在本机跑通。
⚠️ 采集质量的一个硬伤:文献复用 98%(必须改,否则训不出东西)
第一版 50 条记录里 只有 7 篇不同文献,其中 42 条共用同一篇 (C. elegans 基因组论文 PMID 9851916)——84% 的"文献"跟这条 RNA 没关系。
把 --target 提到 1000 后又复现了同一件事(子代理实测):
1000 条里只有 24 个不同 pmid,其中一篇(1987 年 S. pombe 线粒体 LSU rRNA)
一条配了 800 条序列。后果:literature 字段作为监督信号基本无信息量。
根因:esearch 的 relevance 排序是确定性的,第一条 query 一个批次就把 target 填满了
(by_query = {ncrna_refseq: 1000}),后 4 条 query 一次都没跑到;
而每条 RefSeq 记录的 GenBank PUBMED 字段大量指向少数几篇"大论文"。
已加的闸门(10_collect.py):
--max-per-pmid 1:同一篇文献最多给 1 条记录(默认开);--require-mention:DEFINITION 里的特征标识(如(Lncpnky)、(mcmdc2as)) 必须出现在摘要里 —— 这是弱相关性检查,但能把"挂名文献"挡掉。
加闸门后实测:单批 100 个 id 只留下 5 ~ 20 条(之前是 50)。 这正是应该的 —— 宁可少,不能脏。
顺带一个标注噪声线索:那 800 条被引论文讲的是线粒体 LSU rRNA 基因, 这批 RefSeq 条目却注释成 "non-coding RNA … lncRNA"。值得单独看一眼。
下一步
- Qwen3-8B 权重下完 → 跑 11/12/13,出 50 条 RNA-QA,人工抽查质量(PLAN §3 P0 判据)。
- RNA-FM 权重到位 → 跑 §7.1 探针,把 output_shape / 上下文长度落成
reports/rnafm_probe.json。 - bert / pubmedbert 到位 → 跑 §7.3 的 B/C 关(语义指标不变量 + 消融阶梯响应方向)。已完成,见 §7.3 / §7.3b / §7.3c。
- 三者都绿之后,进 P1(Stage1 模态对齐:冻结 RNA-FM + 冻结 Qwen,只训 640→4096 投影)。
评测口径上必须落地的三条(来自 §7.3b,改 04_eval.py 时要照做)
- 报 semantic F1 时必须同时报基线。 每次评测都跑一遍"跨域无关对 + 同域句级随机配对", 把地板跟着主数字一起报。否则读者无法判断读数落在窗口里的什么位置。
-
S_BERT/S_Pub/S_GPT三列不可横向比较。 每列只能和它自己的基线比。 原版表把三个嵌入并排摆,容易被读成"哪个嵌入更好",实际是三个不同的分母。 - PubMedBERT 那列接近饱和,别当主指标。 有效窗口只有 0.048 宽;
报告里应以 ROUGE 为主、
S_BERT为辅、S_Pub仅作参考。
E 轨 Stage 1 进度 + 三个"读 checkpoint"的静默错误(已修)
E 轨 Stage 1 跑到 38.7%
checkpoints/stage1_run1/,27 个 checkpoint,step 500 → 13500(--compress-latent 64)。
10 epoch × 3488 条 = 34880 步,还差 21380 步(~5.9 h @ 1.0 s/step)。
val_loss从 step 6200 起就在 1.365–1.393 横着,train 掉到 0.76–0.91 —— 典型过拟合。proj_std0.32 → 0.39,没有退化成常向量。- Δloss(置零) 峰值 0.2732 @ step1600,衰减到 0.047 @ step13800。
⚠️ 但"Δloss 衰减 = 投影塌缩"这个读法是错的
scripts/31_stage1_rouge_curve.py 的试点(这是论文的指标,才是判据):
| step | ROUGE-1 | ROUGE-2 | ROUGE-L |
|---|---|---|---|
| 500 | 0.2084 | 0.0261 | 0.1337 |
| 8000 | 0.3030 | 0.1234 | 0.1914 |
原论文 Modality Fusion (D&C) = 0.0922 ⇒ 我们是它的 ~3.3 倍。
step-8000 的生成结果里正确写出了 LINC00115 —— 模型确实在读 RNA。
教训(值得单独记一条):Δloss 是逐 token 平均的。答案变长、变准、变多样,
分母变大,Δloss 天然被摊薄。它不能替代 ROUGE 做收敛判据。
Stage1Config 里 collapse_rel_drop=0.75 那套相对判据同样受这个问题影响,
所以 PLAN 里"置零编码器指标明显下降"这条只能当辅助信号,最终以 ROUGE 曲线为准。
修掉的三个静默错误(都是"读 checkpoint 时少装东西",不报错但结果全错)
scripts/22_zero_encoder_probe.py直接崩。Stage1Config(llm_path=args.llm, **(blob["config"]))—— checkpoint 里的 config 含llm_path(和args.llm撞名 →TypeError: got multiple values), 且含collapse_delta这种 dataclass 里根本没有的旧字段。 → 改成只挑dataclasses.fields(Stage1Config)认识的键,命令行优先; 并装回压缩器(以前只 loadprojection,E 轨 checkpoint 的压缩器是随机初始化的)。 同时加了--pass-delta/--weak-delta,并在输出里写明 Δloss 只是参考。src/rnaqwen/models/finetune.py不支持压缩,E 轨没法进 Stage 2。build_stage2_model硬编码Stage1Config(...),encode_samples直接model.projection(rna)—— 压缩器根本没有插入点。 →Stage2Config加compress_latent/compress_method;build_stage2_model传下去、从 blob 装compression、 R/E 配错直接SystemExit报错(不让它静默跑歪);encode_samples在投影前插if model.compression is not None分支(None 时逐位不变);save_stage2一并存compression+compression_spec。scripts/30_infer.py的压缩器权重从来没载过。 原来只有compressor = RnaTokenCompressor(encoder.hidden_size, args.compress_latent)—— 随机初始化,E 轨推理输出看着像样但全是错的。这是最危险的一个。 → 改成从blob["compression"]load,配置自动从compression_spec读, 配错直接报错退出。
冒烟验证(E 轨 Stage 2 全链路,已通过)
[stage2] 载入 Stage1 投影 <- checkpoints\stage1_run1\projection_step13500.pt (step 13500)
[stage2] 载入压缩器 <- checkpoints\stage1_run1\projection_step13500.pt (resampler n_latent=64)
trainable params: 15,335,424 || all params: 8,206,070,784 || trainable%: 0.1869
[stage2] {"step": 1, ..., "loss": 1.3883, "seq_len": 135, "elapsed_s": 3.4}
...
[stage2] 完成 6 步,用时 0.1 min
attn预设的 15,335,424 与静态估算逐位对上(见docs/METRICS.md)。- 压缩后
seq_len135–169(不压缩会是 600–1100)。 - 存回的
projection.pt里compression+compression_spec都在,config.compress_latent = 64,往返一致。 - 实测 Stage 2 吞吐 0.9 s/样本 ⇒ 80,907 条 × 10 epoch ≈ 202 小时。 所以 Stage 2 默认从 3 epoch 起跑(≈61 h),不是 10。
顺手修的第四个:
--kind的 help 写的是open | closed,但数据里的值是open_ended | closed。按 help 写--kind open会筛出 0 条样本。 文档写错选项名 = 静默的空数据集。 已改。
手动运行
RUN.md 是给用户自己跑的命令单(工具 shell 拉起来的长任务已静默死过 5 次以上,
无 traceback、无 abort、事件日志里也没有)。
🔴 重大事故:RNA-FM 权重一直就是坏的,13500 步 Stage 1 全部作废
发现方式:用户贴了一段 RnaFmModel LOAD REPORT 问正不正常。报告本身确实正常
(UNEXPECTED 的是 lm_head.*/ss_head.* 预训练头,MISSING 的是从没用过的 pooler.*)。
但顺手验一下「权重真的装进去了吗」,验出问题。
症状
模型 state_dict 张量数 : 200
checkpoint 原始键数 : 205
剥掉 `model.` 前缀后共有键 : 198 (前缀差异是 from_pretrained 的正常行为,不是问题)
逐位相同 : YES
未覆盖的模型键 : ['pooler.dense.bias', 'pooler.dense.weight'] <- 只有这个
到这里一切正常。继续查:
word_embeddings.weight (28, 640) absmax = 0.000000e+00 nnz = 0 / 17920
序列敏感性:ACGUACGU… vs GGGGGG… max|Δ| = 0.000000e+00
word_embeddings 全零,且不同序列的输出逐位相同。
前向路径是 inputs_embeds = self.word_embeddings(input_ids)(从 RnaFmEmbeddings.forward
的源码确认),全零 ⇒ 输入 = 只有位置编码 ⇒ encoder 完全看不见序列。
量化
总张量 205 / 正常 50 / 全零 155
12 层 encoder 的 attention / intermediate / output 几乎全在零里
根因:文件是对的大小,错的是内容
本地 sha256 38f3ae9aadbf4d3a5ff6873476fe9d16600b83a54711637ec3760e8e83a2408d
上游声明 ec69333eb894dbefb580e690b0cbe1cbae9845fefd2bc78da914cd14226a2828
大小 398,117,588 == 398,117,588 ← 逐字节相同
和 Qwen3-8B 那 4 个 shard 一模一样的故障模式。
现场复现(这次抓到了机制)
重下时实测 .part:
逻辑大小 377,701,268 B ← 只下了 57 MiB,文件却已经接近满
@ 1 MB : 有数据 @ 10 MB : 零填充
@ 30 MB : 有数据 @ 50/100/200/300/350 MB : 零填充
Valid Data Length = 377701268 ← NTFS 认为整段都是「有效数据」,零就是真零
aria2 预分配了整份文件,只有真实分片有数据,其余是零。
(命令行确实带了 --file-allocation=none,aria2 1.37.0;大概率是上一轮中断留下的
预分配文件没被截断,新一轮 --continue=false 从 0 覆写,尾部那些零是上回的。)
中断 + 预分配 = 大小正确、内容一半是零。 这就是全部。
为什么没被发现(三个自己给自己发的假通行证)
from_pretrained不报错。 LOAD REPORT 只提lm_head/ss_head/pooler这三个 无关紧要的键,看起来像"只是架构差异,可以忽略"。- 探针只查形状。
01_probe_rnafm.py/01b_probe_rnafm_limit.py验的是out=(1,83,640)—— 一个全零的模型照样过。我据此写过[OK]。 - 没有 sha256。 Qwen3-8B 坏过一次之后明明立了「单写者 .part + 强制 sha256」的规矩, 但只用在了 Qwen3 上,RNA-FM 漏了。规矩没有强制力,就等于没有。
作废清单
| 东西 | 状态 |
|---|---|
checkpoints/stage1_run1/ 27 个 checkpoint(13500 步 ≈ 3.75 h) |
❌ 废 |
checkpoints/stage1 500/1000/1500、_losscheck、_timing_gc |
❌ 废 |
| ROUGE 曲线试点 0.2084 → 0.3030 | ❌ 废 |
| 「step-8000 正确写出 LINC00115 ⇒ 模型在读 RNA」 | ❌ 收回。encoder 对序列零敏感,这个结论不成立 |
| Δloss 置零探针 0.273 → 0.047 的"塌缩"判读 | ❌ 无意义 |
数据 rnaqa_n6.jsonl / evalset |
✅ 与 encoder 无关,有效 |
| 代码(含本轮修的 4 个 bug) | ✅ 有效 |
| 计时测量 | ✅ 大致仍然成立(encoder 本来就便宜) |
修复
scripts/00e_refetch_rnafm.py(新)——所有通路都走同一个闸门:
下载 -> .part -> sha256 必须 == ec69333e…a2828 -> 才允许原子改名
配套规则(每条都对应一个踩过的坑):
archive_corrupt():坏文件改名留档,不删;- 哈希不符时绝不改名;
- 哈希不符 + 大小已满 ⇒ 判定为「预分配空洞」。这种情况续传救不了
—— 控制文件会说"分片都完成了",每次续传立刻返回同一个坏文件,卡死循环。
必须连
.part.aria2一起清掉; - 哈希不符 + 大小未满 ⇒ 真没下完,保留
.part续传; - 下载完还要独立验一次「换序列输出会变」 —— 哈希对了也可能装错文件。
立规矩
禁止一直换源。 选定一条通路就走到底,慢就慢。 把关的不是速度,是校验。遇到"慢",不要去找更快的源,先把校验补上,然后等。 (已写进 memory.db #1011。)
✅ 换到 ERNIE-RNA(E 轨)+ 下载与健全性检查全过
决定
用户拍板:改用 ERNIE-RNA 作 backbone,理由是结构增强(attention bias / pairwise 表示), 性能强于 BERT 基座的 RNA-FM。 这是 E 轨(PLAN §7)。论文用的是 RNA-FM,所以 ERNIE-RNA 的任何数字都要单独标 E 轨, 不能和论文 / R 轨并列。
取数(三条路都试了)
| 源 | 结果 |
|---|---|
HF multimolecule/ernierna |
✅ 最终成功。345,071,800 B,sha256 02274cb7…08eb,与上游声明逐位一致 |
| hf-mirror.com(并行) | 下到 210/329 MB 后放弃(同一份 artifact,谁先完成算谁的) |
GitHub Bruce-ywj/ERNIE-RNA |
不放权重,只有代码 + 一个 Google Drive 文件 ID |
Google Drive ERNIE-RNA_pretrain.pt |
作者原始 fp32 权重,1,035,221,383 B(1.04 GB),实测传不动。留作兜底 |
| ModelScope | 没有 ernierna。有 ZhejiangLab-LifeScience/{rnafm,rnaernie,rnamsm,rnabert,dnabert2},但都是浙江lab 自己的转换版(vocab 26,键名不同),不是 multimolecule 的 |
健全性检查:PASS
sha256 02274cb7…08eb ✅ 与上游声明一致
张量总数 207,全零 1 ✅ 只有 pooler.dense.bias(非主干,从不使用)
键覆盖 202/202,未覆盖 0 ✅ 权重真的装进模型了
输出形状 (4, 202, 768) ✅ hidden 768
base vs polyG max|Δ| = 3.1138e+00 ✅ 序列敏感
base vs shuffled max|Δ| = 2.3979e+00 ✅ 次序敏感
base vs base max|Δ| = 0.0000e+00 ✅ 确定性
VERDICT: PASS
关键实测差异:ERNIE-RNA 没有 RNA-FM 的 1022 nt 硬上限
RNA-FM position_embedding_type = 'absolute' -> 查表,表只有 1026 行,1023 nt 直接 IndexError
ERNIE-RNA position_embedding_type = 'sinusoidal' -> 算出来的,无硬上限;实测 1030 nt(1032 token)也能过
但 config 的 max_position_embeddings = 1026 是预训练窗口,超出去是外推。
所以 ARCHS["ernierna"].max_nt = 1024(= 1026 token − BOS/EOS),取预训练范围而不是拍一个数。
(现有数据按 1022 过滤过,所以这个数目前不会真的被用到。)
新增/改动
| 文件 | 作用 |
|---|---|
src/rnaqwen/paths.py |
新。集中默认路径 + 轨道归属。R 轨 models/rnafm,E 轨 models/ernierna。覆盖优先级:--encoder > RNAQWEN_ENCODER > 默认 |
src/rnaqwen/models/encoder.py |
重写为多 backbone RnaEncoder,按 config.json 的 model_type 自动识别;arch 与 config 不符、hidden 不符都直接报错 |
scripts/01c_check_encoder_sanity.py |
新。四条闸门(哈希 / 结构 / 键覆盖 / 序列+次序敏感性+确定性)。已用已知损坏文件验证过会 FAIL |
scripts/00d_fetch_verified.py |
新。通用哈希闸门取数;拿不到上游 sha256 就拒绝下载大文件;单写者锁;真实进度读数 |
scripts/01b_probe_rnafm_limit.py |
改为按 arch 解析,不再硬编码 RnaFmModel |
下一步
- Stage 1 冒烟(小样本几步),确认
768 -> 4096的链路通 - Stage 1 全量(E 轨,10 epoch)
- ROUGE 曲线
🔄 转向:彻底放弃 RNA-GPT 软提示路线(2026-10-10)
性质变更:本项目不再是 RNA-GPT 的复现。 变成「RNA-QA 上软提示路线 vs 原生文本路线的头对头对比」。
新计划见 PLAN_v2.md;旧计划留档为 PLAN_v1_softprompt.md。
触发点不是"做不下去",是逐条测出了天花板
用户最初提出「Stage 1 对齐目标与 Stage 2 指令目标脱节」的假设,我核实后发现它是症状不是病根。 真正的病根在核实过程中浮出来:我们冻结了 LLM。
| 检验 | 结果 |
|---|---|
| 投影在丢信息? | ❌ 减掉公共分量后 inter = -0.05,信息完整 |
| 学习式压缩器造成偏执依赖? | ❌ meanpool 与 resampler 逐 token 剖面几乎一致(负值 58.4% / 63.3%) |
| val_loss 能选投影? | ❌ 两次独立配对都是 val 低 ↔ grounding 低 |
| 去各向异性是解? | ❌ val 打平(1.4892 / 1.4926),Δzeroed 差 8× 但消融口径不同、不可比 |
| 原生文本要改 tokenizer? | ❌ 0.526 tokens/nt,扰动中位影响跨度 0 nt,局部稳定 |
四条外部证据(都核实过原文,不是转述)
- ChatMultiOmics arXiv 2412.19191:
directly applying LoRA ... near-random performance in classification and regression tasks ... LoRA primarily leverages pre-trained knowledge to generate well-formed answers based on the output format learned from SFT data→ 精确命中我们的观测:val_loss 降到 1.384(格式学会了),Δzeroed 衰减到 25%(内容没进去) - MAPO arXiv 2605.27741(JHU + 腾讯混元,作者 Sanjeev Khudanpur / Liefeng Bo):
术语 late-stage modality collapse;
abandon the primary source signal in favor of compressed textual priors。其仪器(消融参照的跨模态差分熵)与我们的 Δzeroed 是同一个东西, 但逐 token、更强。 - Moirain arXiv 2605.23961:RNA + 多模态 SFT + DPO;DPO 动机是
without collapsing the learned natural distribution。 - RNA-GPT arXiv 2411.08900:Stage 1 的 Q 留空是故意的,为堵死"抄 prompt 文本"—— 这是它的解,也是它的病。
今天用血换来的六条方法论
mem_get_info()在 gfx1151(APU) 上不可信(报 107 GB,物理内存只 63.6 GB)→ 只看物理内存- 哈希闸门是唯一能挡「大小对、内容空」的手段(已两次)
- 单测过了还要跑真模型冒烟(SOS-LoRA 单测 8/8,冒烟仍抓出 dtype 与作用域两个 bug)
- 「测出来的东西和以为测的不是一回事」今天犯了四次 —— 每次都在 「全零 / max 也零 / 比值太整齐」这类信号上露馅。看到这种信号先怀疑自己。
- 优先用直接量(可分辨性、逐 token),不用聚合标量(val_loss 已被证伪两次)
- 换源不算办法 —— 先修校验,再等
新路线
RNA 序列 → Qwen3 tokenizer 原生 token(0.526 tokens/nt)
→ ① 序列继续预训练(可选,第一版先跳过)
→ ② 指令微调(SFT / LoRA / SOS-LoRA)
→ ③ 推理微调(可选)
保留:4,481 RNA / 92,680 QA 数据集、evalset_merged、评测协议、Qwen3-8B、SOS-LoRA 实现。 零新架构、零 tokenizer 改动,只需要一段几十行的语料预处理。
2026-10-10 小实验:FSQ 的码到底是词表还是哈希(44/45/46)
用户问「能做个小实验吗」。这条路线能不能走,取决于一个不用训 LLM 就能回答的问题: 离散码是不是可复用的词表。跑了三个递进的对照,其中两个指标被我自己的设计否掉了。
44_ 覆盖率 —— 第一次自我否证
v1 用 64 条序列 (29k 位置) + 词表 65536,得到「52% 的码只出现一次」。 这是算术必然,不是发现 —— 平均每码 2.6 个位置,中位重用必然是 1。 v2 改成 256 条序列 (116k 位置)、扫词表 {256,1024,4096,6561,65536} + iid 自助基线。 结果:所有配置的留出覆盖率都略低于基线(4096: 95.8% vs 97.5%)。
45_ 序列指纹对照 —— 第二次自我否证
「低于基线」的唯一解释应是码在序列内成簇。于是加对照: (a) 同序列内位置留出 vs (b) 跨序列留出。
- burstiness:平均游程 1.02(iid 应为 1.0)—— 码在序列内不成簇
- (a) -2.0pp,(b) -2.7pp —— 两者一样
「序列指纹」的解释被推翻。但更重要的:覆盖率这个指标本身没有判别力。 词表 4096、训练位 9.3 万时,任何映射都会让每个码被见到几十次 —— 哈希也会给出 90% 利用率和 95% 覆盖。词表远大于样本时又样本不够。两头都不成立。
46_ 含义迁移 —— 唯一非循环的测试
靶子换成局部 6-mer(来自输入序列,不是 encoder 学的,所以不循环): 在训练序列上学 code -> 最常见 6-mer 的表,在没见过的序列上考。
| 量 | 值 |
|---|---|
| 测试位置中码在训练集见过 | 99.1% |
| 按码预测 6-mer 准确率 | 8.48% |
| 打乱 code<->位置 对照 | 2.95% |
| 常数预测(最常见 6-mer) | 3.02% |
| 增益 / 倍数 | +5.53pp / 2.88x |
打乱对照回落到常数水平(2.95% ≈ 3.02%),说明对照有效。
结论
是词表,但是一个不精确的词表。 码确实携带能跨序列迁移的局部结构信息(2.9x), 不是给每条序列发指纹。但要老实说清两个限制:
- 局部 6-mer 是很弱的靶子 —— ERNIE-RNA 在一位点上编码的远不止 6-mer。 所以 2.9x 是下界,不是上限;但它也说明码与「局部序列组成」的相关性并不强。
- within/重建 err 这两个指标是循环的:FSQ 是确定性映射、AE 又照重构误差训, within<1 是构造保证的。不能拿它们当证据。
对齐踩的坑(记下来)
ERNNIE-RNA 的 tokenizer 每条序列加了 [CLS] 和 [SEP]: encoder 位置数 = 序列长度 + 2。115886 - 115374 = 512 = 2 x 256,一眼可验。 不做这个偏移,k-mer 靶子整体错位一格,结果全是废的。
方法论:一天内第 5、6 次「测的不是我以为的东西」
- 「52% 的码只出现一次」= 词表比样本大(算术必然)
- 「覆盖率低于基线」= 指标无判别力(两头都不成立) 两次都是先看到太干净的数字,然后才去怀疑自己。规则 4 又生效了。
2026-10-10 路线 C 端到端短实验启动(47_)
PLAN_v2.md 已更新:§6.4 第 2 条(去各向异性「前置必需」)被自己的测量证伪,降级为消融项; §6.5 记录 44/45/46 与那两次自我否证;§6.6 记 [CLS]/[SEP] 偏移的坑;§7 换成短实验清单。
脚本 scripts/47_routeC_short_sft.py
单变量:底座冻结 + LoRA(attn) + 一张 4096x4096 可训练码表(16.8M 参数,与臂 A 的 3M 投影同量级)。 RNA 位置不经过 tokenizer,直接用 inputs_embeds 把码 id 映射到码表(不用 resize_token_embeddings, 不污染 Qwen 原词表)。判据是 MAPO 式逐 token 消融,不是 loss —— loss 降下来也可能只是模型学会忽略 RNA。
四路消融:真表 / 零表 / 随机表 / 常数码。关键在对照,不在第一个数:
- vs 零表 低 => RNA 位置上「有东西」在起作用
- vs 随机表 低 => 起作用的是码的内容
- vs 常数码 低 => 起作用的是码的区分度 后两个 ≈50% 就是「Qwen 眼里的 RNA 是乱码」,只是换了形式。
冒烟(60 条 RNA / 30 步)发现的两个问题 —— 都是我的错
- |grad_code| 0.0000 每步都是 0。因为我在 opt.zero_grad() 之后才读 .grad。 打印全废。 修到 step 之前读,实测梯度和 19.65 -> 0.79,码表确实在学。
- 判定逻辑漏了对照。 第一版只看「vs 零表 < 58.4%」就判 ✅。 实际冒烟结果是 vs 零表 32.9%、vs 随机表 48.1%、vs 常数码 47.6% —— 后两个是掷硬币,说明模型用的是「RNA 位置有个可训练向量」而非「这个码是什么」。 差点报出一个假的成功。 判定已改成必须三个对照同时 < 45%。
另一个接线 bug: na_emb 建在 CPU 上(只有 peft 模型走 device_map 上了卡)-> 必须在 ase_emb.weight.device 上建。方法论规则 3:单元测试过 != 接线对。
冒烟的定位
30 步、码表漂移仅 3.64%,对照 ≈50% 是必然的,没有判别力。 冒烟只证明端到端能跑通(梯度非零、初始行范数 1.28 与 Qwen 词表 1.38 同量级、四路前向都通)。
正式短实验(已在后台启动)
1500 条 RNA / 31190 QA,600 步,每 150 步跑一次三路消融。 要看的是对照差距随码表漂移「拉开还是一动不动」 —— 那才是判据。 FSQ 已在 668273 个位置上训完,mse 0.045。