RNAQwen / PROGRESS.md
RomanCohort's picture
add 根目录
37e94a6 verified
|
Raw History Blame Contribute Delete
49.7 kB

复现日志

每条都记 做了什么 / 验收到什么程度 / 踩了什么坑。 只写有证据的部分;没验证的不许写成"完成"。


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 坏了,而字节数一个不差。

成因(复盘):

  1. 这些文件被多轮续传拼过 —— 磁盘写满中断 → 换进程重下 → 再断 → 再续;
  2. 中途还出现过两个进程同时写同一个文件(先起了 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), 这才是它无法分辨对错的根本原因。

落在报告口径上的三条

  1. 不能单独用 semantic F1 判复现好坏。 必须同时报 ROUGE + 同语料的随机/无关基线。
  2. S_BERT 与 S_Pub 不可跨列比较(分母不同)。原版表把两种嵌入并排摆, 容易被读成"BERT 上语义更好",实际不是。
  3. 报 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 上一切正常,说明不是权重问题。

排查过程:

  1. 先怀疑 GPU 被别的进程占着 —— Get-Process 显示没有大进程,可用内存 52 GB,排除;
  2. aria2c 式的环境因素排除(两个解释器都崩);
  3. 等了约 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

两条被实测否掉的链路(写下来省得重走):

  1. pubmed → nuccore(正向):拿到的全是 2–4 kb 完整 mRNA(NM_*), 越过 1024 nt 上限,半小时一条都收不到。
  2. 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):

  1. --max-per-pmid 1:同一篇文献最多给 1 条记录(默认开);
  2. --require-mention:DEFINITION 里的特征标识(如 (Lncpnky)、(mcmdc2as)) 必须出现在摘要里 —— 这是弱相关性检查,但能把"挂名文献"挡掉。

加闸门后实测:单批 100 个 id 只留下 5 ~ 20 条(之前是 50)。 这正是应该的 —— 宁可少,不能脏。

顺带一个标注噪声线索:那 800 条被引论文讲的是线粒体 LSU rRNA 基因, 这批 RefSeq 条目却注释成 "non-coding RNA … lncRNA"。值得单独看一眼。


下一步

  1. Qwen3-8B 权重下完 → 跑 11/12/13,出 50 条 RNA-QA,人工抽查质量(PLAN §3 P0 判据)。
  2. RNA-FM 权重到位 → 跑 §7.1 探针,把 output_shape / 上下文长度落成 reports/rnafm_probe.json。
  3. bert / pubmedbert 到位 → 跑 §7.3 的 B/C 关(语义指标不变量 + 消融阶梯响应方向)。已完成,见 §7.3 / §7.3b / §7.3c。
  4. 三者都绿之后,进 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_std 0.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 时少装东西",不报错但结果全错)

  1. 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) 认识的键,命令行优先; 并装回压缩器(以前只 load projection,E 轨 checkpoint 的压缩器是随机初始化的)。 同时加了 --pass-delta/--weak-delta,并在输出里写明 Δloss 只是参考。

  2. 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。

  3. 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_len 135–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 覆写,尾部那些零是上回的。)

中断 + 预分配 = 大小正确、内容一半是零。 这就是全部。

为什么没被发现(三个自己给自己发的假通行证)

  1. from_pretrained 不报错。 LOAD REPORT 只提 lm_head/ss_head/pooler 这三个 无关紧要的键,看起来像"只是架构差异,可以忽略"。
  2. 探针只查形状。 01_probe_rnafm.py / 01b_probe_rnafm_limit.py 验的是 out=(1,83,640) —— 一个全零的模型照样过。我据此写过 [OK]。
  3. 没有 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

下一步

  1. Stage 1 冒烟(小样本几步),确认 768 -> 4096 的链路通
  2. Stage 1 全量(E 轨,10 epoch)
  3. 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 文本"—— 这是它的解,也是它的病。

今天用血换来的六条方法论

  1. mem_get_info() 在 gfx1151(APU) 上不可信(报 107 GB,物理内存只 63.6 GB)→ 只看物理内存
  2. 哈希闸门是唯一能挡「大小对、内容空」的手段(已两次)
  3. 单测过了还要跑真模型冒烟(SOS-LoRA 单测 8/8,冒烟仍抓出 dtype 与作用域两个 bug)
  4. 「测出来的东西和以为测的不是一回事」今天犯了四次 —— 每次都在 「全零 / max 也零 / 比值太整齐」这类信号上露馅。看到这种信号先怀疑自己。
  5. 优先用直接量(可分辨性、逐 token),不用聚合标量(val_loss 已被证伪两次)
  6. 换源不算办法 —— 先修校验,再等

新路线

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), 不是给每条序列发指纹。但要老实说清两个限制:

  1. 局部 6-mer 是很弱的靶子 —— ERNIE-RNA 在一位点上编码的远不止 6-mer。 所以 2.9x 是下界,不是上限;但它也说明码与「局部序列组成」的相关性并不强。
  2. 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 步)发现的两个问题 —— 都是我的错

  1. |grad_code| 0.0000 每步都是 0。因为我在 opt.zero_grad() 之后才读 .grad。 打印全废。 修到 step 之前读,实测梯度和 19.65 -> 0.79,码表确实在学。
  2. 判定逻辑漏了对照。 第一版只看「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。