gnina源码编译实战:从GPU加速到虚拟筛选的性能调优
我从一个真实场景说起。去年课题组接了个项目靶标蛋白是某个激酶手里攒了80多万个小分子要做虚拟筛选。服务器倒是现成的四张A100但gnina是之前某位同事用conda一把梭装上去的。结果一跑GPU占用率只有不到20%一个分子平均要卡两三秒照这个速度80万分子得跑一个多月完全不现实。后来查下来罪魁祸首就是预编译二进制和服务器CUDA驱动不匹配PyTorch的CUDA stub没对上线GPU明明在那儿gnina却一直在用CPU硬扛。那次之后我学乖了直接在目标机器上从源码编译一劳永逸解决性能、硬件适配和扩展定制的问题。这篇文章就把整个gnina源码编译过程摊开讲包括环境配置、CMake参数、真实踩过的报错、编译后的验证以及一些跑虚拟筛选时才会用到的调优细节。适合手里有NVIDIA GPU、打算认真做大规模虚拟筛选的同学参考尤其是那些被预编译包坑过、想彻底掌控工具链的人。1. 为什么非要自己编译预编译包和GPU驱动之间的隐性矛盾gnina是一个把分子对接和深度学习打分结合起来的虚拟筛选工具底层脱胎于AutoDock Vina但把打分函数换成了卷积神经网络模型配合GPU加速专门用来处理超大化合物库的筛选任务。它的官方GitHub仓库会提供一些预编译的二进制版本听上去很省事但实际用起来你会发现这些二进制跟你手里的服务器环境之间隔着一条很难跨过的鸿沟。1.1 能跑和跑得快完全是两回事预编译gnina的编译环境是开发者自己的他们用的CUDA版本、PyTorch版本、GPU架构和你机器上的驱动不一定对得上。最典型的症状就是你明明有GPU程序的log里也显示CUDA device visible但实际跑起来就是奇慢无比。原因是gnina内部跑神经网络打分时依赖于libtorch而libtorch在运行时需要通过CUDA Driver API去和驱动打交道。如果预编译包链接的CUDA runtime版本和你机器上的驱动版本差太多PyTorch会自动fallback到CPU模式这种失败往往是静默的不报错只是性能断崖式下跌。从源码编译的好处是你可以让整个工具链围绕你的硬件来构建。CMake里明确指定GPU的Compute Capability让CUDA编译器针对你的卡做内核特化生成的代码才能真正把GPU算满。1.2 官方二进制不覆盖的扩展需求另一个促使我编译源码的理由是gnina预编译包里能用的模型和运行模式是固定的。如果你需要自定义CNN模型权重、修改对接盒子定义、或者把gnina的输出格式调整成内部管线的标准格式预编译包基本无能为力。从源码编译之后你可以改代码、改参数默认值甚至可以把一些自己训练的模型权重直接编译进去这在做工业级虚拟筛选时特别重要因为管线里的每一步都要可控、可复现。还有一点预编译的gnina一般会使用某个固定的OpenBabel版本。如果你同时还在用别的开源工具如RDKit两个工具处理同一批分子的结果偶尔会出现细微差别比如芳香性判断、氢原子加成的规则不同。这种不一致在单分子对接时无所谓但在几千几万分子的批量筛选里它会直接污染数据集的一致性。自己编译至少你可以锁定OpenBabel版本和编译参数让整套流程可复现。1.3 编译本身就是一次环境摸底这个可能出乎很多新手的意料。我在把gnina的依赖树一棵棵核清楚、在CMake配置阶段跟各种报错搏斗的过程中相当于把服务器的CUDA生态完整梳理了一遍。哪个库装在了什么位置、GPU算力是多少、C编译器能不能兼容最新的CUDA版本这些东西平时没人关心但一旦你开始做正经的虚拟筛选项目它们全都是瓶颈来源。拉一次源码编译一遍这二十几分钟的投入换来的是后续几个月的安心。2. 编译前的家底盘点依赖库、GPU算力和编译器gnina是个典型的C项目依赖链涉及CUDA Toolkit、libtorch、Boost、OpenBabel、Eigen、CMake。听起来很多但每一样都有明确的作用缺一不可。不要急着动手先把这些依赖捋清楚。2.1 GPU算力与CUDA版本的选择逻辑首先要确定你GPU的Compute Capability。这个数字决定了CUDA代码怎么编译也直接关系到你跑gnina时能不能用上Tensor Core之类的特殊硬件单元。可以用nvidia-smi --query-gpuname,compute_cap --formatcsv查或者直接去NVIDIA官网查表。不同GPU算力对照GPU型号Compute Capability常见服务器A1008.0大规模深度学习节点V1007.0老一代HPC集群RTX 3090 / 40908.6 / 8.9实验室单机RTX 2080 Ti7.5旧工作站L40S8.9专业视觉/AI卡H1009.0最新高性能计算CUDA版本的选择要和驱动匹配运行nvidia-smi右上角能看到Driver Version然后去NVIDIA官网查这个驱动支持的最高CUDA版本。gnina官方目前对CUDA 12.x的支持最省心PyTorch和libtorch也更倾向于新版本所以我建议如果驱动允许直接上CUDA 12.x省掉一堆兼容性问题。2.2 依赖库逐一拆解没有一个是多余的CUDA Toolkit。这是gnina能在GPU上做CNN打分的基础。下载NVIDIA官方runfile安装包时需要注意最好用--toolkit方式装到/usr/local/cuda不要用系统包管理器自动装不然版本被包管理器锁定后续升级很痛苦。libtorch。gnina的CNN推理走的是PyTorch C API也就是libtorch。你需要去PyTorch官网下载对应CUDA版本的libtorch预编译包。注意必须选择C发行版而不是Python的wheel。下载后解压到某个目录比如/opt/libtorch后面CMake配置阶段要把Torch_DIR指到这里。Boost。gnina的源码里大量使用了Boost的程序选项解析和文件系统相关功能。Ubuntu系统可以直接apt install libboost-all-dev但如果你的系统比较老Boost版本过低后面编译会报一些头文件缺失的错误。我自己更推荐从Boost官网下载源码编译因为这样可以控制Boost版本和gcc版本的匹配关系。OpenBabel。gnina依赖OpenBabel来处理小分子的文件格式转换比如SDF、MOL2、PDBQT。这是对接流程里最容易被低估的一环。Ubuntu的apt源里通常有OpenBabel 3.x但版本可能偏旧。建议源码编译OpenBabel 3.1.1以上版本编译时加上-DBUILD_GUIOFF -DBUILD_TESTINGOFF能省不少时间。Eigen。这是个头文件库相当于C版的矩阵运算标准件不需要单独安装只需要把头文件路径指对就行。Ubuntu的libeigen3-dev一般够用。CMake。gnina要求CMake 3.15以上但我建议直接用CMake 3.25以上版本因为新版CMake对CUDA语言支持更成熟处理CUDA_ARCHITECTURES时更智能。2.3 gcc版本和CUDA的兼容矩阵很多人会在这一步翻车。CUDA每一个版本对gcc版本都有明确的支持上限。比如CUDA 12.1支持到gcc 12.xCUDA 12.4支持到gcc 13.x。如果系统默认gcc版本太新CUDA编译器nvcc会报gcc: error: unrecognized command line option之类的错误。写Makefile的时候得特意指定一个兼容的gcc或者在CMake里把CMAKE_CUDA_HOST_COMPILER指到正确路径。我在一台新装的Ubuntu 22.04服务器上就栽过跟头系统gcc是11.4CUDA 12.4没问题但如果当时手贱升级到gcc 13又不想换CUDA就得折腾一大堆。查验方法很简单gcc --version nvcc --version如果gcc版本超出CUDA支持范围最简单的方案是装一个兼容版本的gccsudo apt install gcc-10 g-10然后在CMake时用环境变量指定export CC/usr/bin/gcc-10 export CXX/usr/bin/g-103. CMake配置与源码编译参数逐项解析加真实报错排查这一步是整个过程中的重头戏。3.1 源码获取记得拉全子模块gnina的repo用到了git submodule来管理部分依赖拉取时必须加--recursive参数否则后面编译时会遇到缺失头文件的问题。git clone --recursive https://github.com/gnina/gnina.git cd gnina如果已经clone过但没有拉子模块可以用git submodule update --init --recursive我建议clone之后先看一下当前分支和tag。gnina的stable分支适合生产环境master分支有时候会引入一些实验性的改动编译依赖可能也会变。用tag更稳比如我用的1.1版本git checkout v1.13.2 CMake核心参数逐项拆解进入源码目录后创建build目录开始配置mkdir build cd build cmake .. \ -DCMAKE_BUILD_TYPERelease \ -DCUDA_ARCHITECTURES80 \ -DOPENBABEL3_INCLUDE_DIR/usr/local/include/openbabel3 \ -DOPENBABEL3_LIBRARIES/usr/local/lib/libopenbabel.so \ -DBOOST_INCLUDE_DIR/usr/include/boost \ -DTorch_DIR/opt/libtorch/share/cmake/Torch这里几个参数展开说一下。CMAKE_BUILD_TYPERelease。务必设置。Release模式编译会开启优化选项并且关闭debug断言。gnina在Debug模式下跑得慢到你怀疑人生而且有些GPU内存错误只有在Release模式下才不出现。CUDA_ARCHITECTURES。这个参数指定你的GPU架构直接体现了新CMake对CUDA构建的友好程度。以前老项目用的是CUDA_ARCH或者GPU_ARCH这类自定义变量现在统一走CMAKE_CUDA_ARCHITECTURES但gnina的CMakeLists还兼容旧的CUDA_ARCHITECTURES变量。可以把它设成你GPU的Compute Capability的两位数字比如A100就是80。也可以设成all-major让CMake自动检测并编译所有你机器支持的架构但那样编译时间会变长产物也更大。我建议精确指定一个或者两个架构。假如你有多卡混插比如两台机器分别用A100和3090可以在编译的时候指定成CUDA_ARCHITECTURES80;86注意分号在shell里要加引号cmake .. -DCMAKE_CUDA_ARCHITECTURES80;86OpenBabel3相关参数。如果你是源码编译安装的OpenBabel通常会装到/usr/local下include路径带openbabel3这个子目录库路径就是libopenbabel.so。这两个路径指不对CMake会报找不到OpenBabel或者头文件路径不对导致一堆#include openbabel/...解析失败。Torch_DIR。这个必须指向libtorch解压目录下的share/cmake/Torch子目录。如果不指定CMake有可能会找到你系统里Python环境的PyTorch但那个是Python扩展包不是C库编译时大概率会报各种ABI不匹配的错。值得说明的是这里有个很典型的坑gnina索引BIN里面CUDA相关的库也会用到libtorch的CUDA库如果libtorch的CUDA版本和本机CUDA toolkit版本不一致编译时虽然能通过运行时会报undefined symbol或libc10_cuda.so找不到。所以我强烈建议下载libtorch时严格选择和你CUDA版本对应的那一档。3.3 编译过程中的三个致命报错与处理我整理了自己实际遇到、也见过很多同学遇到的三个典型报错每个都附上排查链路。第一个是CMake阶段报Could not find Torch或Torch_DIR failed to find torch。这个问题90%是因为Torch_DIR没指对地方。很多人以为libtorch解压后直接指到根目录就行但CMake需要的是带cmake配置文件的目录。正确检查方法find /opt/libtorch -name TorchConfig.cmake如果有输出把那个文件的目录路径填到Torch_DIR。如果没有输出说明你的libtorch下载包不完整或者解压出了问题重新解压一遍。第二个是编译时大量报错集中在/usr/include/boost下的文件里错误信息类似error: #error Boost.Math requires C17。这个原因很直接CMakeLists里没有把C标准推到17以上或者你的编译器默认标准太低。gnina在较新版本里要求C17老版本用C14。解决办法是在CMake命令里追加-DCMAKE_CXX_STANDARD17 -DCMAKE_CUDA_STANDARD17如果还报Boost的问题建议检查Boost是否完整安装。有些Linux发行版的boost库被拆成了多个包比如libboost-program-options-dev、libboost-filesystem-dev、libboost-system-dev缺了某一个头文件缺失就会触发各种莫名其妙的错误。第三个是链接阶段报CUDA related库找不到比如libcudart.so: cannot open shared object file。这个一般出现在make的最后几步。原因是源码编译会遇到find_package(CUDA)找不到CUDA库或者CMake找到了但链接器的rpath没设置好。解决办法是确认/usr/local/cuda/lib64在你的LD_LIBRARY_PATH里export LD_LIBRARY_PATH/usr/local/cuda/lib64:$LD_LIBRARY_PATH如果不想每次登录都手动设置写进~/.bashrc。另一种情况是libc10_cuda.so或libtorch_cuda.so找不到那就要检查libtorch版本和CUDA版本的对应关系而不是盲目改路径。官方有兼容性对照表务必对照。3.4 编译加速并行度与内存控制gnina的编译是个比较吃内存的活尤其那些包含CUDA kernel的源文件编译一个文件能吃掉几GB内存。如果你服务器内存只有16GBmake -j32大概率会直接OOM进程被系统杀掉。你需要根据内存来调整并行度。我的一般经验法则是每个并行的CUDA编译任务预留2GB内存CPU-only的编译任务预留1GB。比如64GB内存的机器可以尝试make -j16但更稳妥的做法是先保守地用make -j8编译一会儿用top看看内存占用稳定后再考虑加并行度。另外可以限定CUDA编译使用较少的GPU资源虽然编译过程本身不占显存但如果你同时还有别的训练任务占着卡编译时的CUDA context创建可能会有冲突。通常建议编译时暂时停掉其他GPU任务或者设置环境变量CUDA_VISIBLE_DEVICES让编译进程完全看不到GPU这样它就不会去尝试初始化GPU上下文。完全编译成功的标志是build目录下出现一个可执行文件gnina。可以运行./gnina --help确认基本功能正常。第一次运行会输出一堆编译信息、CUDA版本信息以及可用的GPU列表。4. 编译后的完整验证从会跑到跑得对编译成功只代表能跑距离跑得对还有很长一段路。我见过有人编译完之后直接拿大库去筛结果跑了三天最后发现输出文件里的打分分布明显不对排查半天原来是模型权重没有正确加载gnina静默地退化成了低价打分模式。所以验证这一步不能省。4.1 自带测试集的一键验证gnina的repo里带了测试文件在test目录下。最省事的方法是跑一遍CMake里注册的测试cd build ctest --output-on-failure这个会依次验证基本的对接功能、CNN打分、文件格式转换等模块。ctest的每个测试用例会下载或使用已有测试数据如果网络受限可能有些用例会失败这时候不要慌单独看失败的用例是什么原因。如果是因为下载不了参考文件可以手动去下载放到对应目录再重新跑。如果不想用ctest也可以手动跑一个最简单的场景cd test ../build/gnina -r receptor.pdb -l ligands.sdf --cnn_scoring rescore --out test_out.sdf正常情况下几秒钟内就能看到输出文件sdf里带上了CNNscore和CNNaffinity字段。这两个字段是gnina深度学习打分函数给出的结果CNNscore是结合亲和力的概率打分值越高代表越可能结合CNNaffinity是预测的解离常数pKd值。只有这两个字段出现在输出中才说明CNN推理链路是通的。4.2 小规模虚拟筛选实战跑通输入输出的完整链路跑完自测后我建议再做一个小规模的演练比如拿100个已知活性分子对着一个真实蛋白做标准的虚拟筛选目的不是得到生物学结论而是确认你整个命令行参数配置是符合预期的。gnina最常用的筛选模式是给定受体结构和化合物文件在一个活性位点盒子里做对接打分gnina -r protein.pdb -l ligands.sdf --center_x 12.5 --center_y 24.7 --center_z -8.3 --size_x 22.5 --size_y 22.5 --size_z 22.5 --num_modes 1 --cnn_scoring rescore --out screened_top.sdf这里的--center_x等参数定义了对接盒子也就是蛋白活性位点附近允许配体活动的三维空间范围。尺寸设得太小会漏掉可能的结合模式太大则会显著拖慢速度并增加假阳性。一般经验是根据已知共晶配体的坐标来确定盒子中心盒子边长在20~25埃左右会对大多数靶标有比较好的覆盖。--cnn_scoring rescore表示先用传统打分方式Vina的力场打分做构象搜索再用CNN模型对结果重新排序。这是gnina官方推荐的标准做法比纯CNN对接快很多同时精度比纯Vina高很多。更重的方式是--cnn_scoring full全程都用CNN指导采样但速度会慢不少适合对精度要求极高的场景。输出文件是一个标准的SDF每条分子里包含了对接后的坐标、Vina打分、CNNscore、CNNaffinity。在筛选几万分子时你不可能一个一个看一般会用Python脚本按CNNscore排序取Top 1%做后续的mm_gbsa或者分子动力学验证。4.3 性能对照GPU加速能不能覆盖编译成本很多人掏心窝子的疑问是花了这么大力气编译到底值得吗我用自己的数据说话。在同一台A100上编译好的gnina跑1000个分子的标准筛选Vina构象搜索加CNN重打分每次对接生成5个构象总耗时大约不到5分钟平均每个分子约0.2到0.3秒。而如果用CPU单线程跑同样参数每个分子差不多要7到10秒差了30到40倍。这还是在没有做并发优化的情况下。也就是说编译消耗的1小时时间成本在跑完大概一万个分子之后就完全赚回来了。如果你的化合物库规模是几十万分子这一差距直接决定了你的工期是按周计算还是按月计算。5. 把gnina纳入管线后绕不开的调优细节当你编译好了、验证也通过了下一步就是把这把快刀接入自己的筛选管线。这里分享几个我在实战里总结出来的细节这些内容在官方文档里几乎不会写得那么细。5.1 高吞吐筛选的并发策略与资源限制gnina能跑多快除去GPU本身的算力很大程度上取决于你的CPU和GPU之间是否平衡。预处理文件读入、加氢、格式转换是CPU密集的而对接构象搜索和打分是GPU密集的。当GPU算力很强但CPU核数不足时GPU会出现明显的空闲等待表现就是GPU利用率起伏很大。要把CPU和GPU的利用率同时拉满合理做法是让gnina在GPU上批处理配体。gnina内部本身就会尝试把多个分子打包成一个batch送进神经网络但你得给它足够的内存。默认情况下每个进程处理一个分子文件如果你的化合物库是拆分好的一个个SDF文件可以采用多进程方式每个进程占一个GPUfor i in {1..4}; do CUDA_VISIBLE_DEVICES$((i % 4)) gnina -r receptor.pdb -l ligands_$i.sdf ... done wait这个方案要注意的是4个进程同时跑每个进程占用的显存和GPU SM资源会互相争抢实际加速比不是线性的。我实测下来A100上跑2个并行进程总吞吐比单个进程提升约1.6到1.8倍跑到4个进程只比2个提升约20%~30%。瓶颈已经从GPU转到了PCIe带宽和CPU预处理。所以不要盲目开几十个并行进程先摸清你机器的瓶颈在哪里。5.2 运行时的常见错误速查表整理几个高频运行错误的快速定位和应对方式错误现象主要原因处理方式CUDA error: no kernel image availableCUDA_ARCHITECTURES和GPU算力不匹配重新编译时指定正确的算力terminate called after throwing an instance of std::bad_alloc分子文件过大一次性加载过多拆分分子文件控制单文件分子数量Error reading input ligand配体文件格式问题缺少3D坐标使用OpenBabel或RDKit预处理生成3D结构Path does not exist: ./docked.sdf输出路径不可写或目录不存在提前建目录并确认权限Cannot find gnina model file模型权重路径没有指定在源码目录下运行或用--model指定权重路径No CUDA compatible device found驱动没装好或容器没挂载GPU检查nvidia-smi容器内加--gpus all这些错误里no kernel image available是最典型的。有一次我在一台机器上编译时忘了指定CUDA_ARCHITECTURESCMake默认生成的是当前机器GPU架构的代码结果把二进制拷到另一张不同算力的卡上去跑就报这个错。所以如果要跨机器分发gnina二进制一定要在编译时把目标机器所有GPU架构都包含进去或者干脆每台编译一次。5.3 模型权重管理与多模型切换gnina的可执行文件和模型权重是分离的。源码编译出来的gnina默认会从源码目录下的models子目录加载权重但实际跑的时候经常需要切换不同模型比如用--cnn_scoring rescore时可以用-m model_file参数来指定权重文件。我习惯把常用的模型权重集中放到同一个目录通过环境变量或者脚本统一指定。比如说export GNINA_MODEL_DIR/data/models/gnina gnina ... --cnn_scoring rescore --model $GNINA_MODEL_DIR/gnina_rescore.h5这样做的好处是方便版本管理。虚拟筛选项目里不同批次的结果要能够横向对比如果模型权重换了一个文件而没人记得整个项目的结论就可能变得不可信。我建议使用语义化的文件名包含模型类型、训练时间和数据集的描述不要让模型名字跟git hash脱钩。5.4 版本升级和后续维护的注意事项gnina是一个活跃更新的项目新版本通常会引入新的模型架构或修复已知bug但升级时也要付出代价。我见过有人直接从1.0跳到1.2结果旧版跑出来的结果和新版完全无法对齐因为CNN权重的参数空间变了、打分分布也跟着变同一批分子两个版本打分排名差异很大。所以如果你要把gnina升级到新版本我建议不要直接在原来的编译目录上覆盖而是保留旧版本的build目录新建一个新的源码树来编译新版本两套二进制共存一段时间。在完成一个周期的虚拟筛选后用新版本对旧的Top结果做一次复筛看看是否有系统性的偏移再决定是否正式切换。我自己现在的做法是在服务器上维护了/opt/gnina/1.1、/opt/gnina/1.2这样的目录结构每个版本括号filename里带着构建时间切换版本时只需要改一下PATH或者用绝对路径调用。相比某些商业软件开源工具这种简单的多版本共存方案反而很省心。还有一个容易忽略的维护点是依赖库的升级。如果你重新编译了OpenBabel或者升级了libtorch那么gnina的二进制虽然不会主动坏掉但它依赖的.so文件会指向新库。如果新库API不兼容gnina运行时会报error while loading shared libraries。这也是我建议尽量使用源码编译而不是环境大杂烩的原因因为你能追踪到每个依赖的版本来源。写在最后的一点体会从源码编译gnina这件事本质上不是为了那几十分钟的编译过程而是为了掌握运行环境的主动权。预编译包总归是别人替你做了一半决定而虚拟筛选这种计算密集型任务效率和安全都藏在细节里。基础环境的搭建、编译参数的合理设置、错误日志的认真排查这些看似繁琐却决定了后续几个月的工作能不能顺利推进。希望这篇里写的编译流程和那些排查思路能帮你少走一些和我一样的弯路。如果你在编译或者跑筛选过程中遇到了别的奇怪问题也别灰心静下心来一层层拆解依赖关系大部分问题都能在日志里找到真正的原因。