☰
分子生成扩散模型IRDiff复现:环境依赖到采样评测避坑
2026/10/28 20:14:35 网站建设 项目流程

简介:面向药物设计与分子生成方向的研究者与工程实践者,这份 IRDiff 完整评测文档围绕基于蛋白质-配体相互作用的检索增强 3D 分子扩散模型展开,重点解决论文代码复现中的环境配置、模块缺失与运行报错问题。内容不仅梳理 IRDiff 的方法背景与模型框架,还给出修正后的可运行项目代码、训练好的模型、测试案例,以及代码报错位置和修改方法,便于读者按特定蛋白或口袋体系生成目标感知分子,并计算 vina_score、vina_docking_score、qvina_score、QED、SA 等指标。文档同时支持基于自定义数据集进行微调与训练,并附有个人分析标注,可作为理解检索增强与自增强机制、排查复现问题的参考。资源包共 1 个 PDF 文件,约 12.23MB,结构紧凑,适合按评测流程查阅。目前已有 121 人学习。

1. 建环境就翻车:分子生成项目里最耗时的从来不是模型

「分子生成」「扩散模型」「药物设计」这三个词凑在一起的项目,读代码之前先得把环境过一遍。IRDiff 这套评测资源把踩过的坑原样摊开了:conda 解不出依赖、pip 阶段找不到 autodocktools-py3、采样脚本缺 einops、配置里全是别人机器上的绝对路径。这些报错单看都不算难,难在你不知道下一个在哪儿等你。我按自己复现的顺序,把每个报错的现象、根因、改法一条条写清楚,你对着命令抄,就能从建环境一路跑到出 vina_score、QED、SA 这些指标。

2. conda 解不出环境:cryptography 与 openssl 的版本互锁

conda env create -f irdiff.yml跑了几十秒,终端吐出一棵长长的树,最后一句是Could not solve for environment specs。新手第一反应往往是「conda 坏了」或者「网络又抽风」,然后开始换镜像、重装 conda、清缓存。真正的原因要朴素得多:环境文件里同时写了两条互相打架的约束,求解器只是在告诉你它无解,不是在说它出故障了。

这类问题在复现老论文代码时出现频率极高。作者导出环境的那台机器、那个 channel 状态、那套平台构建产物,和你手上的并不是一回事;环境文件里的每一行都是从某次conda env export直接 dump 下来的,带着精确到编译产物的锁,换台机器就未必成立。

2.1 报错现场:这棵冲突树到底在说什么

先把原始报错贴出来,逐段读。

LibMambaUnsatisfiableError: Encountered problems while solving: - package cryptography-37.0.1-py38h9ce1e76_0 requires openssl <1.1.2a, Could not solve for environment specs The following packages are incompatible ├─ cryptography ==37.0.1 py38h9ce1e76_0 is installable and it requires │ └─ openssl <1.1.2a , which can be installed; └─ openssl ==3.1.0 hd590300_2 is not installable because it conflicts with the above

读法是这样的:├─开头的是「你要装的包」,│ └─缩进下去的是这个包带进来的附加约束,└─是另一条独立约束,最后告诉你这两条无法同时满足。这里的第一条是 cryptography 37.0.1 的这个构建产物,它声明自己只能在 openssl 小于 1.1.2a 的环境里工作;第二条是环境文件里明确钉死的 openssl 3.1.0。一个要大版本号小的,一个钉死了大版本号新的,求解器当然只能摊手。

值得留意的是py38h9ce1e76_0这一串。这是 conda 的 build string,编码了 Python 版本、编译器和构建序号,意味着这个包是给特定平台、特定解释器编译出来的二进制。环境文件里只要带上这串,换平台就等于换了一个包,而那个包在目标平台上可能压根不存在,或者依赖关系完全不同。

2.2 定位到第 42 行:删版本号而不是升级 openssl

知道冲突在哪儿,改法有两条路。第一条是把 openssl 降到 1.1.2a 以下,去迁就 cryptography;第二条是把 cryptography 的版本锁松掉,让它自己去找一个能和 openssl 3.1.0 共存的构建。评测文档里选的是第二条,这也是我处理同类问题时的默认选择。

原因很实际:openssl 3.x 不只是 cryptography 一个包在用,后面 pip 阶段还要装的 vina、pdb2pqr、meeko 这一串二进制轮子,很多都是按新 openssl 编译的。为了迁就一个老包的构建串把 openssl 降下去,等于把后面一连串依赖的基础抽掉,属于按下葫芦浮起瓢。相比之下,cryptography 本身只是个通用加密库,换个版本对项目的实际功能没有影响。

改动落在irdiff.yml第 42 行附近,把三段式锁定拆成单包名:

# irdiff.yml 节选 name: IRDiff channels: - pytorch - pyg - conda-forge - defaults dependencies: # 改前:三段式锁死,版本 37.0.1 + 平台构建串,要求 openssl < 1.1.2a # - cryptography=37.0.1=py38h9ce1e76_0 # 改后:只保留包名,交给求解器挑一个与 openssl 3.1.0 兼容的构建 - cryptography - openssl=3.1.0

参数说明:conda 的=分段是有含义的,包名=版本是锁版本,包名=版本=build是连编译产物一起锁死。只留包名的时候,求解器会在满足其余约束的前提下挑最新可用版本;写两段的时候锁定主版本但允许不同 build;写三段的时候基本等于把包当成一个不可替换的固定件。跨机器复现老项目,遇到PackageNotFound或者这种 Unsatisfiable,优先怀疑三段式锁。

改完 yml 之后有一个容易忽略的动作:先删掉半成品环境。conda env create失败时可能已经创建了空目录,直接重跑会报prefix already exists。

# 1. 清掉失败留下的半成品,避免前缀冲突 conda env remove -n IRDiff # 2. 重新求解并创建;-n 显式指定环境名,不依赖 yml 里的 name 字段 conda env create -f irdiff.yml -n IRDiff

顺带说一句环境名。原始文件里写的是ipdiff,而资源和模型都叫 IRDiff,手动改成IRDiff之后,后面所有conda activate和路径假设才对得上。名字看着是小事,但当你有三四个同类项目并行时,激活错环境的代价是半小时起步。

2.3 求解慢和求解失败是两回事

改完 yml 之后重跑,可能还是等很久,终端停在Solving environment不动。这时候要分清两类情况:一类是真的在算,一类是已经卡死。新版 conda 默认用 libmamba 求解器,速度比老式求解器快一个数量级,但遇到大量 channel 混合(pytorch、pyg、conda-forge、defaults 四个一起开)时仍然会算一阵。我的经验是给它十分钟,超过十分钟还没动静,就换成经典求解器再看输出,报错信息往往更具体:

# 换回经典求解器重试:速度慢,但冲突报告更细,便于定位 CONDA_SOLVER=classic conda env create -f irdiff.yml -n IRDiff

装完之后,不要急着往下跑代码,先做两个基本确认:

conda activate IRDiff # 确认解释器版本与 CUDA 可用性:后者为 False 时,后面采样一定失败 python -c "import sys, torch; print(sys.version); print(torch.__version__, torch.cuda.is_available())" # 确认 conda 与 pip 装的东西没有互相覆盖 python -m pip list | grep -iE "torch|pyg|geometric"

第二行那个torch.cuda.is_available()特别值得单独看。它返回 False 有两种常见原因:机器上没有可用显卡,或者 conda 装下来的 torch 是 CPU 版。前者无解,后者说明 channel 优先级把 CPU 构建排到了前面,需要在 yml 里给 pytorch channel 更高的优先级后重建。这一步如果跳过,后面的报错会出现在采样脚本里,看起来像代码问题,实际是环境装错了,排查成本翻倍。

3. pip 阶段崩在 autodocktools-py3:No matching distribution found 怎么绕

conda 那一关过了,conda env create走到最后一步却红了:CondaEnvException: Pip failed。这个报错特别容易被误读成「环境创建失败」,然后整个删掉重来。实际上conda env create是两个阶段:先由 conda 解依赖、装 conda 包,装完之后再启动一个 pip 子进程去装 yml 里pip:段列出的东西。报错发生在第二阶段,说明 conda 那部分已经装好了,环境里东西是全的,只是 pip 那几条没装上。

理解这一点很关键,它决定了你要不要付出重建环境的代价。答案是不用,直接激活进去手动补。

3.1 为什么 conda 全过了,最后一步 pip 才炸

把 pip 子进程的输出捞出来看,核心就两行:

Pip subprocess error: ERROR: Could not find a version that satisfies the requirement autodocktools-py3==0+unknown ERROR: No matching distribution found for autodocktools-py3-py3==0+unknown failed

0+unknown这个版本号是整件事的线索。它不是任何人在 PyPI 上发布的版本,而是某次从源码本地安装之后,pip freeze把「没有版本元数据」这件事序列化成的占位符。作者在自己的机器上装完,导出环境时把这行原样写进了 yml,换到别人机器上,pip 去索引里找一个叫0+unknown的发行版,当然找不到。

第二层原因是 AutoDockTools 这个包本身。它是一个老牌的对接预处理工具套件,长期没有在 PyPI 上发布配套的 wheel 或 sdist,只有在第三方维护的一个 py3 分支里才有可安装的源码。所以即便把版本号删掉写成autodocktools-py3,pip 大概率还是找不到。

3.2 进环境手动补:源码构建 + 四个配套依赖

既然 pip 找不到发行版,就换成从源码构建。这一步要在已经激活的环境里做,确保包装进的是 IRDiff 而不是 base。

conda activate IRDiff # 从上游仓库直接构建安装:PyPI 上没有对应发行版,只能走源码 # <上游仓库> 替换为评测文档中给出的 AutoDockTools py3 分支地址 python -m pip install "git+https://github.com/<上游仓库>/AutoDockTools_py3.git"

这条命令背后做了三件事:clone 仓库、跑setup.py构建 wheel、把构建产物装进当前环境的 site-packages。成功时终端会打印Successfully built AutoDockTools-py3和一行Successfully installed AutoDockTools-py3-1.5.7...,带一个 git 提交号后缀。看到这个后缀就说明装的是源码快照而不是正式发行版,属于预期行为,不是装错了。

装完之后补剩下的四个依赖。这四条的版本号是评测过程中验证过能互相共存的组合,不要随手改成最新:

# 对接与性质计算链路:meeko 负责配体准备,vina 提供打分,pdb2pqr 处理质子化, # propka 预测 pKa,mmcif-pdbx 处理 mmCIF 格式的结构文件 python -m pip install \ "meeko==0.1.dev3" \ "pdb2pqr==3.6.1" \ "vina==1.2.2" \ "propka==3.5.0" \ "mmcif-pdbx==2.0.1"

参数说明:meeko==0.1.dev3里的dev3表示这是开发版预发布版本。有些环境里 pip 默认会跳过预发布版本,如果提示找不到匹配版本,加--pre再试。pdb2pqr==3.6.1和propka==3.5.0是一对,前者依赖后者做 pKa 预测,版本错配会在质子化阶段抛AttributeError。vina==1.2.2是打分和对接的核心,vina_score、vina_docking_score 这些指标全靠它算出来。

装完做一次冒烟验证,确认这几个二进制能正常 import:

# 逐个 import,任何一个失败都会在后续评测阶段以更难读的形式爆出来 python -c "import vina, meeko, propka, pdb2pqr; print('docking stack ok')"

3.3 pyg 必须停在 2.0.4:训练才暴露的隐性雷

评测文档里专门提了一句:pyg 的版本要用 2.0.4,其他版本训练时可能出问题。这句话看着像随口一提,实际是血泪经验——因为这个问题在安装阶段完全不报错,只在训练跑起来之后以 loss 变 NaN 或者权重加载报 missing keys 的形式出现,极难联想到是图神经网络库版本的问题。

先确认当前版本:

# 查看 torch_geometric 及配套扩展的实际版本 python -c "import torch_geometric as pyg; print('pyg', pyg.__version__)" python -m pip list | grep -iE "torch-scatter|torch-sparse|torch-cluster"

如果版本不是 2.0.4,回退:

# 回退到项目验证过的版本;--force-reinstall 确保覆盖已有的高版本 python -m pip install "torch-geometric==2.0.4" --force-reinstall

为什么偏偏是这个版本?pyg 在 2.x 的几个小版本之间改过Data和Batch的字段拼装逻辑、MessagePassing的聚合默认行为,以及部分模型的导入路径。IRDiff 里 PMINet 和扩散主干都建在 pyg 的消息传递层上,蛋白原子和配体原子要在同一张 K 近邻图里传播特征,任何一处聚合语义的微调都会让隐藏状态的数值范围偏移,训练几百步之后梯度就发散成 NaN 了。

这里还有一个隐藏依赖:torch-scatter、torch-sparse、torch-cluster这几个扩展必须和 torch 的版本、CUDA 版本严格对齐。它们不走 pip 的通用 wheel,装错了会在 import 阶段就报 undefined symbol。确认顺序是先定 torch,再装对应版本的扩展,最后装 pyg。顺序反了,pyg 会把不匹配的扩展版本一起拉下来。

4. 采样脚本起不来:ModuleNotFoundError 与写死路径的双重拦截

环境装完,模型权重和数据也放到位了,敲下采样命令,终端直接甩一个 traceback。这一段通常有两个坑连着踩:先是缺模块,补完之后再跑,又因为配置里的绝对路径找不到数据。前者一秒能改,后者要翻配置文件。

4.1 报错停在 attn.py 第 4 行:einops 缺失为什么建环境时不报

原始报错是这样的:

Traceback (most recent call last): File "sample_split.py", line 17, in <module> from models.molopt_score_model import ScorePosNet3D, log_sample_cate File ".../models/molopt_score_model.py", line ... from models.attn import CrossAttention, RetAugmentationLinearAttention File ".../models/attn.py", line 4, in <module> from einops import rearrange, repeat ModuleNotFoundError: No module named 'einops'

读 traceback 要读最底下那行,那才是真正的失败点。上面几层只是导入链:入口脚本导入模型文件,模型文件导入注意力模块,注意力模块第一行就要用 einops。einops 是个张量重排的小工具库,做rearrange、repeat这类维度变换极其顺手,diffusion 类项目的注意力实现几乎离不开它。

为什么建环境的时候没报?因为 conda 只负责把 yml 里列出的包装上,pypi 依赖树里的间接依赖它不会主动推导。而 einops 不在 yml 的依赖清单里,只在代码被 import 的那一刻才暴露。这类「运行时才炸的缺失模块」有个共同特征:报错位置永远在某个深层模块的第一两行 import 处,而不是在你以为的主流程里。

补装很简单:

# conda-forge 里就有,走 conda 装能和已有的 numpy 保持 ABI 一致 conda install -c conda-forge einops # 如果 conda 源里版本不满意,退回 pip 也行 # python -m pip install einops

补完之后建议顺手把项目里所有顶层 import 扫一遍,提前发现同类的缺失,省得一个一个撞:

# 静态扫出项目内部 import 的第三方包名,和已安装列表做比对 grep -rhoE "^(import|from) [a-zA-Z0-9_]+" --include="*.py" ./models ./src ./datasets \ | awk '{print $2}' | sort -u

把输出和pip list对一遍,明显不在列表里的就是下一个 einops。这个动作三分钟,能省掉后面反复中断重跑的时间。

4.2 三处路径改写:eval_path、training.yml 的 path 与 split

模块补齐之后,接下来撞的是路径。项目里的默认配置写的是作者机器上的绝对路径,形如/path/to/eval_results/,换台机器必然找不到。

第一处是评测脚本里的结果目录:

# cal_metrics_from_pt.py 第 8 行 # 改前:作者机器上的绝对路径,换机器直接失效 # eval_path = '/path/to/eval_results/' # 改后:相对项目根目录的路径 eval_path = './eval_results/'

第二处和第三处在训练配置里,两行必须一起改,只改一行会在加载数据集时报键不存在的错误:

# configs/training.yml 第 4-5 行 # 改前 # path: /path/to/crossdocked_v1.1_rmsd1.0 # split: /path/to/crossdocked_pocket10_pose_split.pt # 改后:指向项目内 ./data 目录 path: ./data/crossdocked_v1.1_rmsd1.0 split: ./data/crossdocked_pocket10_pose_split.pt

参数说明:path指向的是处理好的 lmdb 数据库目录,里面存的是口袋和配体的坐标、原子类型;split指向的是一个.pt文件,里面是训练/测试集的划分索引。两者必须来自同一次预处理,混用不同版本的 split 文件会出现索引指向了 lmdb 里不存在的条目,报KeyError而不是文件找不到,排查方向会完全跑偏。

这里有个特别容易翻车的细节:相对路径的基准是「你敲命令时所在的工作目录」,不是脚本文件所在目录。所以这三处改成相对路径之后,必须在项目根目录执行python sample_split.py ...,不能在./src里执行python ../sample_split.py。后者会让./data解析到./src/data,报一模一样的文件不存在。

4.3 跑通内置案例:两个 index、一个 batch_size 和显存账

路径理顺,就可以跑内置测试案例了。评测资源里内置的 0 号体系是一个具体的蛋白-口袋组合,拿它做第一发验证最合适,因为它有已知的参考配体,出问题容易对照。

python sample_split.py \ --start_index 0 \ --end_index 0 \ --batch_size 25 \ --test_prompt_indices_path ./src/test_prompt_ligand_indices_top3.pt \ --result_path ./results/result_0

参数逐个说:--start_index 0和--end_index 0一起构成要处理的蛋白编号区间,两个都写 0 表示只跑第 0 号这一个体系;跑批量评测时把它们改成首尾编号即可。--batch_size 25是一次前向同时处理多少个样本,直接决定显存占用。--test_prompt_indices_path指向参考配体的索引文件,文件名里的top3对应每个口袋取 3 个高亲和力参考配体,这个数字和论文里的 k 是同一个概念。--result_path是生成结果的落盘目录。

显存这块值得单独算一笔。实测下来,batch_size 设成 25,显存占用大约 19 GB,生成 100 个分子耗时约 50 分钟。这里不要做简单线性外推,因为模型权重、CUDA context、cuDNN 的 workspace 本身就要吃掉几个 GB,剩下的才是随样本数增长的部分。24 GB 的卡上,25 是比较稳的上限;16 GB 的卡建议降到 16 以下;想快一点往上加之前,先用小 batch 跑一轮看nvidia-smi的峰值,再决定加多少。

采样行为由configs/sampling.yml控制,默认内容如下:

model: checkpoint: ./pretrained_models/pretrained-IRDiff.pt sample: seed: 2021 # 固定随机种子,保证同一配置下结果可复现 num_samples: 100 # 每个口袋生成的分子数量 num_steps: 1000 # 扩散去噪步数,步数越少越快但结构质量下降 pos_only: False # 是否只生成坐标、不生成原子类型 center_pos_mode: protein # 以蛋白质心为原点,等变性的前提 sample_num_atoms: prior # 原子数从先验分布采样,而非固定值

center_pos_mode: protein这一项不能随便改。模型靠把蛋白质心平移到零来保证 SE(3) 等变性,改了之后生成的分子的相对位置会整体偏移,打出来的 vina 分数会系统性变差,但代码不会报错——又是一个「不报错但结果错」的坑。num_steps: 1000是质量与时间的直接权衡,降到 500 能省一半时间,代价是成环率和键长分布都会退化。

5. 避坑清单:五个让我重跑过的坑

前面四章是按顺序踩的坑,这一章是横着切的。这五条不分先后,共同点是都会让你白跑一轮,而且报错信息往往指向错误的方向。

5.1 采样跑完却没有 sdf:eval 只存了 metrics

现象:采样脚本打印Sample done!,评测脚本也跑完,./eval_results/下多了metrics_*.pt,但翻遍整个目录找不到任何分子结构文件。想拿生成结果去做可视化或者二次对接,无从下手。

原因:评测脚本的设计目标是算指标,它把有效性、键长分布、各项打分都塞进一个 dict 存成.pt,中间生成的构象对象用完就丢了,没有落盘的代码路径。这不是 bug,是原作者的取舍,但对想复用生成结果的人来说是个硬缺口。

解决:在eval_split.py结尾的保存分支里补一段构象导出。找到if args.save:那个块,在torch.save之后追加写 sdf 的逻辑:

if args.save: # 原有逻辑:把指标字典存成 .pt,方便后续统计分析 torch.save({ 'stability': validity_dict, 'bond_length': all_bond_dist, 'all_results': results }, os.path.join(result_path, f'metrics_{args.eval_step}_{eval_step}.pt')) # 新增:把生成的分子的构象写成 sdf,供可视化与二次对接使用 sdf_path = os.path.join(result_path, 'Generated_molecules.sdf') with Chem.SDWriter(sdf_path) as writer: for mol in generated_mols: if mol is not None: writer.write(mol)

注意generated_mols这个变量名要按你实际脚本里的名字替换,不同版本可能叫mols或all_mols;Chem来自 rdkit,脚本里通常已经 import 过。写完之后检查 sdf 的条数是否等于num_samples,条数明显偏少说明有分子在构象构建阶段被丢掉,那批分子多半是无效结构,属于正常损耗。

5.2 batch_size 一加就 OOM:显存和样本数是线性关系

现象:单体系跑通了,想着一次多跑几个体系提高效率,把--batch_size从 25 提到 40,跑到一半进程被杀,终端只留一个Killed,没有任何 Python traceback。

原因:Killed是操作系统 OOM killer 干的,不是 Python 抛的异常,所以看不到栈。显存不够时 CUDA 一般会抛torch.cuda.OutOfMemoryError,但如果是主机内存被吃光(比如 lmdb 读取缓冲加 dataloader 预取),表现就是这种无栈退出。两种 OOM 的现象很像,要分开确认。

解决:先用nvidia-smi看一眼显存峰值,确认是显存还是内存。显存问题就降 batch;主机内存问题就降 dataloader 的 worker 数。判断当前安全上限的经验做法是从 8 起步,翻倍试探:

# 试探显存上限:先 8,再 16,再 25,每档跑 2 个体系观察峰值 for bs in 8 16 25; do nvidia-smi --query-gpu=memory.used --format=csv -l 1 > "mem_${bs}.log" & python sample_split.py --start_index 0 --end_index 1 --batch_size ${bs} \ --result_path ./results/probe_${bs} kill %1 done

跑完对比三个 log 里的峰值,留 15% 余量就是你机器上的安全值。别迷信别人报的数字,显存占用和驱动版本、CUDA 版本、是否开了 TF32 都有关系。

5.3 建环境中断后重跑:prefix already exists

现象:conda env create跑到一半 Ctrl+C,或者网络断了,重新执行同一条命令,报CondaValueError: prefix already exists,但conda activate进去发现一堆包缺失,处于半死不活的状态。

原因:conda 创建环境是先在envs/下建目录、写元数据,再往里面装包。中断发生在装包阶段,目录已经存在但内容不全,第二次创建时检测到目录就拒绝继续,不会自动覆盖。

解决:删掉再建,别想着在原环境上打补丁——半装状态下哪些包完整哪些不完整根本说不清。

# 列出所有环境,确认目标环境的准确名字 conda env list # 彻底删除半成品环境 conda env remove -n IRDiff # 确认目录真的没了(conda 偶尔会留下空壳) ls -d ~/anaconda3/envs/IRDiff 2>/dev/null || echo "clean" # 重新创建 conda env create -f irdiff.yml -n IRDiff

另外,conda env create中途失败后 pip 阶段装的东西也会一起丢掉,所以重建之后要从第 3 章的手动 pip 步骤重来一遍。这也是为什么我在第一次建环境成功之后会立刻conda env export > irdiff_working.yml存一份,以后再重建直接用它,省掉全部试错。这份「后悔药」在并行折腾多个项目的时候价值极高。

5.4 换自定义口袋就 KeyError:split 文件和 index 对不上

现象:内置案例跑得挺顺,换成自己的蛋白-口袋体系之后,采样脚本在加载数据那步抛KeyError,键值是一串看起来像哈希的字符串,或者直接IndexError: list index out of range。

原因:--start_index和--end_index不是文件系统里的序号,而是split文件里测试集列表的下标。自定义数据如果没走同一套预处理流程生成新的 split 文件,下标就会越界;如果混用了新旧两个版本的 split,下标能对上但指向的是另一个体系,报的是 KeyError。评测文档里提到修正后的代码支持按自定义数据集微调训练,前提就是数据要按同样的预处理口径走一遍。

解决:先确认 split 文件里到底有多少条,再决定 index 范围:

# 检查 split 文件的测试集规模,确认 index 的合法范围 import torch split = torch.load('./data/crossdocked_pocket10_pose_split.pt', map_location='cpu') print({k: len(v) for k, v in split.items()}) # 典型输出包含 train / test 两类 print(split['test'][0]) # 看第一条的结构,确认字段名

拿到test的长度之后,--end_index最多填到len-1。自定义数据集就按同一格式重新生成一份 split 再指过去。这一步做完再跑,报错会从 KeyError 变成正常的缺文件提示,说明索引已经对上了。

5.5 升级 pyg 后训练 loss 变 nan

现象:环境里 pyg 被别的项目带着升到了更新的版本,微调训练跑了三四百步,loss 从正常值缓慢爬升然后突然变成nan,之后所有参数都是 nan,模型彻底废掉。推理(只做采样)不受影响,所以很容易以为是数据问题。

原因:pyg 在 2.x 的几个版本之间改过消息传递的聚合语义和Batch对象的字段拼装方式。IRDiff 的 PMINet 要在蛋白原子和配体原子构成的一张 K 近邻图上做跨分子注意力,聚合结果差一点点,经过一千步扩散去噪的累积放大就是数值爆炸。版本号只差一个小版本,代码一行没改,行为就不是同一个了。

解决:钉死版本,并且在每个新环境建好之后立刻验证。

# 训练前强制校验版本,不匹配就直接退出,别等跑几百步才发现 python - <<'PY' import sys import torch_geometric as pyg expect = "2.0.4" if pyg.__version__ != expect: sys.exit(f"pyg version mismatch: got {pyg.__version__}, expect {expect}") print("pyg ok:", pyg.__version__) PY

顺带一个训练期的观察技巧:在训练循环里加一行梯度范数打印,nan 出现之前通常会先看到范数从个位数跳到几百。看到这个苗头就停下来查版本和图构建,比等 loss 变 nan 再回滚要省时间。

6. 出结果之后:三步定位这次到底挂在哪一环

跑通一次之后,后面每次失败都应该能快速归位。我的习惯是把整条链路切成四段——环境、数据、采样、评测——每段的失败信号长得完全不一样,先看信号属于哪一段,再进那一段细查。

症状大概率所在环节第一件该做的事
UnsatisfiableError/No matching distribution环境看冲突树最底层那条约束,回 yml 删三段式锁
ModuleNotFoundError环境(运行时补装)读 traceback 最后一行,装完再扫一遍同类 import
FileNotFoundError/KeyError数据确认工作目录是项目根,再核对 split 与 lmdb 是否同源
Killed无栈退出采样分别看显存峰值和主机内存,别只看一个
跑完无产物 / 指标异常评测检查 sdf 导出是否补过,指标口径是否用了 vina 而非 qvina
loss 变 nan训练先核 pyg 与 scatter/sparse 版本,再看梯度范数历史

比表格更管用的是二分法。一次几十分钟的采样,不要每次都从头跑:先用--start_index 0 --end_index 0 --batch_size 8跑单个体系的最小复现,确认这一步能出结果,再去调大范围。如果单体系正常、多体系挂,问题在批处理或资源;如果单体系就挂,问题在数据或环境。这个二分能把排查范围砍掉一半以上。

还有一个小习惯:每次跑之前把关键配置打印一份出来存到结果目录。

# 把本次运行的实际配置、环境版本、显存状态一起留档,出问题时有据可查 mkdir -p ./results/result_0 conda list > ./results/result_0/env_snapshot.txt cp configs/sampling.yml ./results/result_0/sampling_used.yml nvidia-smi > ./results/result_0/gpu_at_start.txt

这份快照在事后复盘时是黑匣子级别的存在。同一个体系昨天出 -6.0 的 vina 分,今天变成 -4.8,先别怀疑模型,翻出两份sampling_used.yml对一遍,十有八九是num_steps或者参考配体索引文件被换了。

从那以后我每次复现这类带扩散采样的项目,都强制走一遍「单体系最小复现 → 存配置快照 → 再放大批量」的流程,哪怕时间紧也雷打不动。踩过的坑不会因为你着急就绕开你,只会因为你没留档而多踩一遍。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询