ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

科研计算避坑指南:从环境配置到算力调度的实践总结

科研计算避坑指南:从环境配置到算力调度的实践总结 做科研计算这件事有时候真的就像开盲盒。你以为写好脚本点下回车就能等结果结果等来的往往是满屏报错红字、OOM、Segmentation Fault或者更气人的——程序安静地跑完了产出的结果却怎么看怎么不对劲。老吴坐标在某高校课题组这几年主要做材料体系的分子动力学模拟和机器学习势函数训练日常就是跟 Linux 服务器、conda 环境、Slurm 调度器、GPU 显存这些东西打交道。这篇“踩坑记”算是把近几年反反复复掉进去又爬出来的坑统一做个复盘。写出来一是给自己备忘二是让刚进组的师弟师妹以及所有在科研计算路上挣扎的朋友们少走点弯路。这些坑单独看都不算多大但叠加在一起轻则浪费一个周末重则白烧几千核时。全文不绕弯子直接按“环境、算力、数据、复现”四个重灾区来拆最后附上我现在固定使用的工作流能抄就抄。1. 环境配置开局一把conda后面全靠眼泪1.1 环境隔离是科研计算的第一道防线先说环境。刚读研那会儿我对 conda 的理解就是装包工具什么都用 conda install装不上就用 pip再装不上就 sudo apt install 直接怼进系统 Python 里。后果可想而知今天装了这个包把那个包依赖顶掉明天升级 Python 版本又把一堆旧包干废。最惨的一次我把 base 环境里的一套科学计算库搞乱了结果组里好几个项目脚本全都跑不起来折腾了两天才发现是 scipy 版本从 1.7 被悄悄升到 1.11接口行为变了。后来我养成了一个很土但很管用的习惯每个项目单独建一个 conda 环境名字就用项目代号比如 md446、nequip_test 这种。环境文件用 environment.yml 或者 requirements.txt 把版本锁死换机器的时候一条命令就能还原。如果你现在还在 base 环境里裸奔真的建议你赶紧停下来开一个新环境把项目迁过去。环境隔离是科研计算的第一道防线这条防线如果破了后面所有的坑都会加倍放大。1.2 CUDA 版本不匹配PyTorch 教你做人第二个高频坑是 CUDA。老吴第一次在组里服务器上跑深度学习势函数训练的时候照着网上教程一条 conda install pytorch 装完一跑就报错说 CUDA 不可用torch.cuda.is_available() 老老实实返回 False。查了半天才发现PyTorch 默认装的是 CPU 版本得用专用镜像源装 CUDA 版。那会儿不懂以为有显卡就能自动调用白白折腾一个晚上。后来换到另一台机器torch.cuda.is_available() 倒是 True 了但跑训练时直接崩掉提示 CUDA driver version is insufficient。一看才发现显卡驱动支持的最高 CUDA 版本是 11.x而 PyTorch 编译用的 CUDA 是 12.1。这时候要么升级驱动要么装低版本 PyTorch。踩了这次坑之后我每次都先 nvidia-smi 看一下驱动版本再根据驱动支持的 CUDA 版本去选对应 PyTorch 和 CUDA 的组合。还有个很容易被忽略的细节TensorFlow、PyTorch 或者手写 CUDA 扩展会依赖不同的 cuDNN 版本版本不匹配时常报 undefined symbol 这类诡异错误。遇到这种问题别瞎猜先查 LD_LIBRARY_PATH 里指定的库路径再对比编译时用的库版本八成能定位。1.3 动态库与编译器看不见的坑最要命说起 LD_LIBRARY_PATH我真是一把辛酸泪。有段时间组里一台机器上我明明已经把某个库装好了Python 里 import 也正常但只要用 mpirun 跑并行就报 libmpi_xxx.so.0: cannot open shared object file。后来发现是 MPI 自己编译链接的库路径没写进环境变量或者写了别人的路径和当前 MPI 版本冲突。解决方案也不复杂先 which mpirun 确认自己用的是哪一个 MPI再检查依赖库是否齐全最后把对应的库目录加入 LD_LIBRARY_PATH并尽量放在靠前的位置。编译器版本也是同样的道理。用老掉牙的 gcc 4.8 去编译现代 C 代码报错内容经常是“未声明标识符”这种误导性信息其实本质是标准库太旧不支持新特性。比如 C17 的 std::filesystem老版本编译器上就是编译不过。所以项目初始化之前先 echo $PATH、gcc --version、python --version、nvidia-smi 全部确认一遍把人、代码、依赖库、硬件版本都对齐了才能少踩坑。2. 算力调度GPU 排队最怕作业跑了半天才报错2.1 Slurm 脚本的三个经典死法课题组现在的计算基本都是通过 Slurm 调度器提交到集群上跑。Slurm 脚本看着简单实际坑不少。第一种死法内存申请太小程序跑到一半被 OOM 杀掉。跑分子动力学模拟时体系规模和内存需求并不是简单线性关系原子数翻倍、邻居列表构建方式不同内存占用可能差十几倍。我吃过的一个亏是脚本里写 --mem16G但体系有几十万个原子跑几步就被 job killed。第二种死法是队列时间限制估算失误。集群默认给每个作业 12 小时我们的训练任务一般要一两天结果经常早上看作业没了日志最后一行是 CANCELLED AT TIME LIMIT。后来学乖了先拿小规模数据估算单步耗时再乘以总步数留出 1.5 倍余量写进 --time 参数里。第三种死法是 GPU 资源申请了但没用上。比如 Slurm 脚本里申请了 --gresgpu:1但 Python 进程里没正确设置 CUDA_VISIBLE_DEVICES结果所有任务全挤到 GPU0 上。这不仅是性能浪费还经常直接 OOM。正确做法是在脚本里显式 export CUDA_VISIBLE_DEVICES$SLURM_JOB_GPUS或者读取 Slurm 传入的 GPU 编号来绑定。我把最常见的问题和解决办法整理成了一张表贴在工位上也方便你直接对照排查症状多半原因排查手段作业被 cancel日志末尾 time limit--time 申请太短小样本估算总耗时后乘 1.5作业跑一会就被 killed--mem 申请不够 或 代码有内存泄漏用 /usr/bin/time -v 统计最大 RSS多卡任务所有进程挤在 GPU0CUDA_VISIBLE_DEVICES 没设置脚本里 export CUDA_VISIBLE_DEVICES$SLURM_JOB_GPUS提交后一直排队不上申请的节点/特性集群里不足检查 sinfo 查看分区空闲情况输出全是乱码或结果写入失败工作目录没有写权限将输出写到 $SLURM_TMPDIR 再拷贝回来2.2 OOM 不是世界末日显存优化三板斧训练深度学习模型时被 OOM 打断真的是家常便饭。我的第一板斧是把 batch size 先减半往往立刻能跑起来。但 batch size 太小影响训练稳定性所以第二板斧是调整混合精度。现在主流框架都支持 AMP自动混合精度用 FP16 算前向和反向关键操作保持 FP32显存能省将近一半速度还快不少。我印象特别深的是训练一个等变图神经网络势函数一开始怎么都塞不进 24G 的卡开了混合精度后不仅跑进去了速度还快了 30%。第三板斧是梯度累积。当你实在不想减 batch size又显存有限可以把一个大 batch 拆成几个小 batch每个小 batch 算完梯度先不更新累积几个再统一更新一步。这相当于用时间换空间训练效果接近大 batch。不过梯度累积有个注意点loss 的计算要做相应缩放BatchNorm 这类层在累积模式下效果会打折扣要结合自己的网络结构来权衡。2.3 多卡并行DP 省事但 DDP 才是正解组里真正开始多卡训练是在数据量上来之后。最初图简单直接用 PyTorch 的 DataParallelDP一行代码就能用多卡。后来才发现 DP 内部实现是参数广播加上梯度汇总通信开销巨大而且不同 batch 的分布也容易造成梯度不稳定。换到 DistributedDataParallelDDP之后速度提升非常明显。但 DDP 启动方式不太一样需要在代码里 init_process_group还要用 torchrun 或者 mpirun 来启动刚开始总是不顺手。关于 DDP 我有一个特别想提醒的坑多卡训练时的学习率。如果原本单卡用 lr1e-3现在上了 8 卡线性缩放的话 lr 就要调整到 8e-3 附近但具体最优值还得重新调不能想当然。还有DDP 的随机数种子要确保每个进程不一样否则所有进程吃同一份数据顺序等于白并行。我早期调试时因为固定了全种子结果 8 张卡跑出来的 loss 曲线一模一样浪费了一周算力才想明白是种子设置的锅。3. 数据处理你以为是程序 bug其实是数据在作怪3.1 浮点数精度大数相减精度丢到外婆家做科研计算对数值精度要有一种本能的敏感。我在处理一批从实验数据重整出来的坐标文件时需要算两个坐标之间的差值。有个坐标分量的数量级在 1e5 左右另一个也在 1e5 附近两者相减结果应该只有几十。但如果用 float32 存这个计算结果的精度就会非常惨因为 float32 只有大约 7 位有效十进制数字1e5 级别的小数点后两三位根本存不下来。这类问题经典到几乎所有数值计算教科书都会讲但实操中还是很容易犯。我自己的判断标准是如果计算过程里涉及大小差异很大的数字加减或者需要累加特别多小量比如求积分、求和尽量用 float64如果只是为了喂给深度学习模型做输入float32 足够。另外比较两个浮点数是否相等时千万不要用 a b要写 abs(a - b) eps 这种形式否则科学计数法下的一点点误差都会让你怀疑人生。3.2 小文件地狱IO 也能卡到怀疑人生有一段时间我要处理一批配置文件每个文件只有几十 KB但数量有几十万个。直接全部读进内存内存会炸一个个读又慢得让人崩溃一个遍历脚本跑了一晚上没跑完。后来意识到瓶颈根本不在 CPU而在磁盘随机 IO因为系统每打开一个小文件都要做一次独立的元数据操作。解决办法有两个方向。一是把这些文件打包成一个大文件比如用 tar 归档或者转成 LMDB、HDF5 这种适合随机读取的格式训练时一次性载入速度快几十倍。二是如果文件确实没有二次读取需求就换成顺序读的方式或者用 multiprocessing 多进程并行读取把 IO 调度起来。老吴现在的习惯是所有结构化数据只要确定要反复用就先转成 HDF5 或者 LMDB这个转换花的时间后面一定省得回来。3.3 周期性边界条件与文件格式细节是魔鬼做分子动力学模拟时周期性边界条件的处理是重灾区。如果原子坐标跨过了盒子边界而你又在保存文件时忘了做 unwrap 操作后期分析链长、扩散系数结果就是错的。这种错不是报错是“悄悄错”最可怕。我踩过的一个具体例子是计算均方位移MSD时没有处理原子跨边界发生的跳跃导致扩散系数算出来比文献值大了两三个数量级。文件格式也有讲究。不同的软件和脚本可能对原子类型、电荷序、末尾换行符的解析互不兼容经常出现“我读出来的坐标和你看的不是一回事”的情况。我现在的做法是写一个校验脚本读入文件后打印前几行和原子总数再加上坐标范围检查、能量守恒检查在正式计算前先花五分钟验证数据完整性。这个习惯帮我挡掉了很多次周末才发现“算了五天白算”的惨案。4. 调试与复现让每个结果都说得出口4.1 随机种子不是救命稻草很多同学遇到复现问题第一反应就是“设置随机种子”。确实如果全部用 CPU 计算把 Python、NumPy、PyTorch 的随机种子都固定好很大概率能复现。但问题是 GPU 上的某些操作比如一些 cuDNN 卷积算法是非确定的用了相同的种子两次前向的结果也可能有细微差别这通常不影响一个完整训练流程的统计特性但如果你要做精确到小数点后好几位的核对就麻烦了。我遇到过最诡异的场景是同样的代码、同样的数据、同样的种子今天跑出来的结果和昨天不一样。后来发现原因在于 cuDNN 的算法选择会根据硬件负载自动变化torch.backends.cudnn.deterministic True 和 torch.backends.cudnn.benchmark False 写上去之后这种差异就消失了。当然这会牺牲一点速度换来的是完美复现在需要严谨对比的实验里很值。说到种子设置我后来写了个模板每次训练前统一处理一遍import random import numpy as np import torch def set_seed(seed: int 42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False4.2 版本锁定与环境快照防止结果“漂移”做科研计算代码和依赖环境的版本锁定特别重要。有次我拿一年前写好的脚本重新复现结果怎么算都不对查了很久才发现是当时的 NumPy 被后来装的新包升级了底层 BLAS 实现都变了同样的操作结果直接不同。从那时起我每个项目跑完一批重要结果都会用 conda env export environment.yml 把环境导出并存档代码里记录 Python 包版本号甚至把关键依赖的 wheel 文件备份到一个目录里。虽然看起来有点小题大做但一旦遇到“结果对不上”的纠纷这些记录就是救命稻草。我个人现在的标准做法是项目根目录放一个 manifest.txt里面记录操作系统、Python 版本、每个依赖包版本以及关键硬件信息。配合日志里记录 git commit 号能让一个结果彻底“说清楚是怎么来的”。做研究的朋友应该都明白可复现性就是科研的生命线这个坑怎么强调都不过分。4.3 调试前的侦探工作先用最小样本跑通科研计算里有个很反人性的现象越大的计算越不能直接上。我见过太多人搞了个大体系一提交就是几千核跑了一个月发现居然是初始条件写错了。老吴自己的血泪教训是任何新代码、新脚本、新流程都必须先用最小样本快速验证。比如模拟体系先放几百个原子跑几步确认能量守恒、力场解析正确、输出文件正常再放大到完整规模。最小样本测试还有一个好处就是能迅速暴露显存、内存、IO 这些资源瓶颈因为小规模跑起来很快调试迭代也就快。等确认小样本没问题再花资源跑大任务这时候才能安心去睡觉。有人觉得这样麻烦但我算过一笔账一次大规模计算失败浪费的可能是一两千核时的资源而最小样本验证只需要十几分钟这笔账怎么算都划算。5. 老吴式总结这几条军规是拿命换的5.1 我现在固定使用的标准工作流到了现在我每次启动一个新的科研计算任务基本遵循一套固定流程。第一步单独建 conda 环境用 environment.yml 记录依赖。第二步写一个数据校验脚本对输入数据做完整性、边界、精度检查。第三步用最小样本跑通流程确认逻辑无误。第四步提交大规模任务之前反复检查 Slurm 脚本的资源申请包括内存、GPU 数量、时间限制。第五步任务跑起来之后设置监控定期检查日志确保没有静默出错。这套流程看起来繁琐但一旦跑熟了能挡住 90% 以上的低级错误。我最近在带新来的硕士生要求他第一周就先把这套流程跑一遍后面项目推进速度快了不少。说到底科研计算最贵的是你的时间和注意力而不是机时本身很多“省时间”的偷懒最后都变成了更贵的返工。5.2 心态与工具少熬夜多打日志最后想聊聊心态。科研计算经常要和“神秘失败”作斗争那种黑盒一样的感觉确实折磨人。我的体会是遇到诡异问题不要硬刚先搜索常见错误信息看看是不是已知的坑多打日志把每个关键步骤的输出记录下来善用 tmux 或者 screen 让长时间任务挂在后台重要数据多备份一份最好备份到不同物理设备上。如果非要给刚入坑的朋友总结三条军规那就是第一环境永远隔离第二结果必须可复现第三大规模计算前必须小样本验证。这三条是老吴用无数个不眠之夜换来的信我能少踩一半坑。
返回列表