拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

gnina源码编译实战:从GPU加速到虚拟筛选的性能调优

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-gpu=name,compute_cap --format=csv查,或者直接去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_GUI=OFF -DBUILD_TESTING=OFF,能省不少时间。

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.x,CUDA 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.4,CUDA 12.4没问题,但如果当时手贱升级到gcc 13,又不想换CUDA,就得折腾一大堆。

查验方法很简单:

gcc --version nvcc --version

如果gcc版本超出CUDA支持范围,最简单的方案是装一个兼容版本的gcc:

sudo apt install gcc-10 g++-10

然后在CMake时用环境变量指定:

export CC=/usr/bin/gcc-10 export CXX=/usr/bin/g++-10

3. 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.1

3.2 CMake核心参数逐项拆解

进入源码目录后,创建build目录,开始配置:

mkdir build && cd build cmake .. \ -DCMAKE_BUILD_TYPE=Release \ -DCUDA_ARCHITECTURES=80 \ -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_TYPE=Release。务必设置。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_ARCHITECTURES=80;86,注意分号在shell里要加引号:

cmake .. -DCMAKE_CUDA_ARCHITECTURES="80;86"

OpenBabel3相关参数。如果你是源码编译安装的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 C++17"。这个原因很直接,CMakeLists里没有把C++标准推到17以上,或者你的编译器默认标准太低。gnina在较新版本里要求C++17,老版本用C++14。解决办法是在CMake命令里追加:

-DCMAKE_CXX_STANDARD=17 -DCMAKE_CUDA_STANDARD=17

如果还报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内存。如果你服务器内存只有16GB,make -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文件,可以采用多进程方式,每个进程占一个GPU:

for 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_ARCHITECTURES,CMake默认生成的是当前机器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这件事,本质上不是为了那几十分钟的编译过程,而是为了掌握运行环境的主动权。预编译包总归是别人替你做了一半决定,而虚拟筛选这种计算密集型任务,效率和安全都藏在细节里。基础环境的搭建、编译参数的合理设置、错误日志的认真排查,这些看似繁琐,却决定了后续几个月的工作能不能顺利推进。希望这篇里写的编译流程和那些排查思路,能帮你少走一些和我一样的弯路。如果你在编译或者跑筛选过程中遇到了别的奇怪问题,也别灰心,静下心来一层层拆解依赖关系,大部分问题都能在日志里找到真正的原因。

返回列表