分子模拟异构算力适配开发教程(8):OpenMM 插件开发——registerPlatforms 与 registerKernelFactories 的双导出协议
版本声明块
- 工具/软件:OpenMM 8.x(CustomCPPForceImpl 需 8.1.0+);C++ 工具链(CMake)
- 语言/环境:C++(插件本体)+ Python(加载与调用)
- 本文目标:读完你能写出一个“能被 OpenMM 加载、能注册一个自定义平台”的最小插件,并理解加载时序为什么是“先全部 registerPlatforms 再 registerKernelFactories”
一句话结论:OpenMM 平台插件是一个动态库,必须实现 PluginInitializer.h 声明的两个 C 导出函数extern "C" void registerPlatforms()与extern "C" void registerKernelFactories(),放在默认目录(安装目录lib/plugins,或OPENMM_PLUGIN_DIR指向的路径)由Platform.loadPluginsFromDirectory()加载;新建平台四件套 = Platform 子类 + KernelFactory 子类 + 每个 Kernel 的 KernelImpl 子类 + 注册代码。
〇、本篇要解决的认知问题
- OpenMM 插件的物理形态是什么?默认放在哪、怎么被加载?
- 为什么必须是两个 C 导出函数?加载时序“先全部 registerPlatforms 再 registerKernelFactories”解决什么问题?
- 新建一个 Platform 需要写哪几类代码?不实现某些 Kernel 会怎样?
- CustomCPPForceImpl(8.1.0+)为什么能把“写自定义力插件”的代码量降一个数量级?
一、机制解析
1.1 插件的物理形态与加载路径
为什么这一节对你重要:第 4 篇讲了“运行期注册”的 API 面,本篇下到 C++ 层看这个 API 面的实现机制——它是国产平台适配(第 10 篇 openmm-musa)的工程地基。
官方 Developer Guide 第 3 章(Writing Plugins)的定义:插件就是一个动态库(Linux 上 .so、Windows 上 .dll、macOS 上 .dylib),放在 OpenMM 安装目录的lib/plugins下。加载方式两条:
// C++ 侧Platform::loadPluginsFromDirectory(Platform::getDefaultPluginsDirectory());Platform::loadPluginLibrary("/path/to/myplugin.so");// 单独加载一个getDefaultPluginsDirectory()读环境变量OPENMM_PLUGIN_DIR(未设则用安装目录约定)。Python 侧等价调用:Platform.loadPluginsFromDirectory(Platform.getDefaultPluginsDirectory())——很多发行版的 OpenMM Python 包在 import 时自动做这一步,所以用户通常感觉不到插件加载的存在。
一个重要的历史注脚:AMD 的 HIP 平台就曾以独立插件分发(amd/openmm-hip仓库,conda 渠道-c streamhpc -c conda-forge),8.2.0 才收编进主线——“插件 → 主线”是 OpenMM 生态吸纳新硬件的标准路径。国产平台走插件路线,既是技术方案,也是向上游贡献的最短路径。
1.2 双导出协议与加载时序
插件必须实现PluginInitializer.h声明的两个 C 导出函数:
extern"C"voidregisterPlatforms();// 创建 Platform 实例并注册extern"C"voidregisterKernelFactories();// 注册 KernelFactory为什么是 C 链接(extern "C"):动态库的导出符号要跨编译器/ABI 稳定查找,C++ 的名字修饰(name mangling)会破坏这一点——两个函数必须以未修饰的 C 符号暴露。
为什么是两个函数而不是一个:官方文档明确了批量加载时序——加载器对目录里所有插件先逐个调用 registerPlatforms(),全部完成后再逐个调用 registerKernelFactories()。设计意图:允许“跨插件补内核”——插件 B 的 KernelFactory 可以给插件 A 注册的 Platform 添加 Kernel。如果边注册平台边注册内核,就没有机会做这种组合。这个时序决定了你的插件代码结构:registerPlatforms 里只做“平台注册”,KernelFactory 注册全部放到 registerKernelFactories——不要在一个函数里做完。
1.3 新建平台四件套
官方 Developer Guide 的清单(第 3 章 3.1 节 Creating New Platforms):继承 Low Level API 的抽象类——
- Platform 子类:平台本体,声明名字、属性(如 Precision/DeviceIndex——第 4 篇的属性面就是这里定义的)、支持的内核清单;
- 一个或多个 KernelFactory 子类:内核工厂,平台收到“给我一个 XXX 内核”请求时由工厂生产;
- 每个 Kernel 的 KernelImpl 子类:内核实现(力的计算、积分的一步……);
- 注册代码:registerPlatforms() 里
new MyPlatform然后Platform::registerPlatform(p)(注意:是 registerPlatform,addPlatform 不存在——第 4 篇的纠错)。
关键的宽容性设计(官方原文的意思):不实现的 Kernel 只是意味着该平台不能跑“需要该 Kernel 的模拟”——创建 Context 时会抛异常,而不是库加载失败。也就是说,你的国产平台插件可以渐进式实现:先把非键力的内核做出来,PME 内核慢慢补——没做之前,用你平台的模拟要么换 CPU PME(如果实现了那个路径)要么报“不支持”。这个设计让“先跑通再完善”的移植策略成为可能。
Kernel 的查找机制也有官方原语:Platform.findPlatform(kernelNames)(第 4 篇提过)就是按“平台声明支持的内核集合”过滤的——你的平台声明了哪些 Kernel,findPlatform 就怎么匹配你。
1.4 CustomCPPForceImpl:把自定义力从“每平台一份内核”降到“一份 C++”
OpenMM 8.1.0 的 release note 引入CustomCPPForceImpl:“new piece of low level infrastructure for use when writing plugins… implemented entirely in platform-independent C++… the amount of code needed for plugins of that sort is dramatically reduced”(插件代码量戏剧性减少)。
背景痛点:自定义力(CustomForce 家族)在传统架构下每个平台要各写一份内核(CUDA 一份、OpenCL 一份、CPU 一份……),新平台没实现就是“不支持”。CustomCPPForceImpl 把“力的计算逻辑”放在平台无关的 C++ 层(CPU 上执行,结果回传),让任意平台(包括你还没写内核的国产平台)都能跑这个自定义力——代价是该力不享受 GPU 加速。
这对国产适配的价值:先让平台能跑全部功能(CPU 兜底自定义力),再逐步把热路径内核 GPU 化——与 1.3 节的宽容性设计组合成完整的渐进式移植策略。8.5.0 更进一步:PythonForce(Python 写力函数)+ GPU 端能量最小化。
二、完整代码与逐行剖析
一个最小可加载的“探测平台”插件——不实现任何物理内核,只验证插件协议全链路(编译 → 放目录 → 加载 → 注册 → 按名查找)。这是任何国产平台插件的第一块脚手架:
// ProbePlatform.h/.cpp —— 最小 OpenMM 平台插件骨架// 教学目的:跑通"双导出协议 + 四件套"的完整链路,物理内核后续逐个补。// 编译:见文末 CMakeLists;产物 ProbePlatform.so#include"openmm/Platform.h"#include"openmm/PluginInitializer.h"#include"openmm/internal/PlatformImpl.h"// KernelFactory 等基类#include<cstdio>#include<map>#include<string>#include<vector>usingnamespaceOpenMM;// ── 四件套之一:KernelFactory(生产内核的工厂)──────────────────────// 探测平台不实现任何物理内核——工厂存在但生产目录为空。// 真实平台(如国产 GPU):这里为每个内核名创建对应 KernelImpl。classProbeKernelFactory:publicKernelFactory{public:// 基类虚函数:平台请求内核时调用。name 是内核标识(如 "IntegrateVerletStep")。// 未实现的内核返回空实现或抛异常——官方语义:该平台跑不了需要此内核的模拟。KernelImpl*createKernelImpl(std::string name,constPlatform&platform,ContextImpl&context)constoverride{throwOpenMMException("ProbePlatform 尚未实现内核: "+name);}};// ── 四件套之二:Platform 子类(平台本体)────────────────────────────classProbePlatform:publicPlatform{public:ProbePlatform(){// 属性面在这里定义(第 4 篇 getPropertyNames() 返回的就是这些)。// 探测平台声明两个示意属性——真实平台按硬件能力声明(如 Precision/DeviceIndex)。platformProperties.push_back("ProbePrecision");// 平台属性名注册.setPropertyDefaultValue("ProbePrecision","single");// 默认值(字符串!第 4 篇的类型约定)}conststd::string&getName()constoverride{staticconststd::string name="Probe";// 平台名字符串:getPlatformByName('Probe') 的键returnname;}// 声明支持的内核集合:空 = 什么都跑不了(探测平台的诚实声明)。// 真实平台:把已实现的内核名逐个 push 进 supportedKernels。boolsupportsKernels(conststd::vector<std::string>&kernelNames)constoverride{returnfalse;// 诚实返回:没实现任何内核}// Context 创建钩子(真实平台在这里初始化设备/队列——对照第 5 篇 DeviceStreamManager)voidcontextCreated(ContextImpl&context,conststd::map<std::string,std::string>&properties)constoverride{std::printf("[ProbePlatform] context created with %zu properties\n",properties.size());}// ... 其余纯虚函数按需实现(编译器会告诉你缺什么——骨架期可给空实现)};// ── 四件套之三+四:注册代码(两个 C 导出)──────────────────────────staticProbeKernelFactory*factory=nullptr;extern"C"voidregisterPlatforms(){// 时序约束(官方协议):这里 ONLY 注册平台。// registerPlatform 是唯一入口(addPlatform 不存在)。Platform::registerPlatform(newProbePlatform());}extern"C"voidregisterKernelFactories(){// 时序约束:加载器在所有插件的 registerPlatforms 之后才调这里。// 给已注册的 Probe 平台挂内核工厂——"跨插件补内核"的机制落点。for(inti=0;i<Platform::getNumPlatforms();++i){Platform&p=Platform::getPlatform(i);if(p.getName()=="Probe"){if(factory==nullptr)factory=newProbeKernelFactory();p.registerKernelFactory("*",*factory);// "*":默认工厂(真实平台按内核名分组注册)break;}}}# CMakeLists.txt —— 插件构建(核心片段) cmake_minimum_required(VERSION 3.16) project(ProbePlatform CXX) set(CMAKE_CXX_STANDARD 17) find_package(OpenMM REQUIRED) # 需要 OpenMM 的 CMake 配置(发行包或源码安装) add_library(ProbePlatform SHARED ProbePlatform.cpp) target_link_libraries(ProbePlatform PRIVATE OpenMM::OpenMM) # 产物丢进插件目录(或用 OPENMM_PLUGIN_DIR 指到这里): # cmake --install . --prefix ~/.openmm-plugins 然后 export OPENMM_PLUGIN_DIR=~/.openmm-plugins# load_probe.py —— 加载与验证(Python 侧全链路测试)fromopenmmimportPlatform# 手动加载(绕开发行版的自动加载,确保我们确实在测这个 .so)libs=Platform.loadPluginsFromDirectory("/path/to/plugin/dir")print("加载了:",libs)names=[Platform.getPlatform(i).getName()foriinrange(Platform.getNumPlatforms())]print("平台清单:",names)assert"Probe"innames,"Probe 平台未注册——检查两个导出函数与加载时序"p=Platform.getPlatformByName("Probe")# 按名查找成功 = 协议跑通print("属性面:",list(p.getPropertyNames()))# 应含 ProbePrecision逐段剖析:
- 两函数的分工严格遵守官方时序:registerPlatforms 只 new+注册平台;KernelFactory 的挂接全部推迟到 registerKernelFactories——
registerKernelFactory("*", *factory)的查找发生在“所有平台已注册”之后,这正是跨插件补内核机制能工作的前提。把两步合并写会破坏这个协议。 supportsKernels诚实地返回 false:探测平台不撒谎(官方语义“不实现就是不支持”)。真实国产平台在这里维护 supportedKernels 集合,findPlatform()的协商就靠它。setPropertyDefaultValue("ProbePrecision", "single")印证第 4 篇的规则:属性值是字符串、属性面在 Platform 类里声明。- Python 测试脚本用手动
loadPluginsFromDirectory而非自动加载——把“插件真的被加载了”变成显式断言,这是插件开发的第一个习惯:每一步都有可验证的成功标准。
对照表:四件套与真实国产平台的对应(第 10 篇的预告):
| 本篇骨架 | openmm-musa 类真实工程 |
|---|---|
| ProbePlatform::getName → “Probe” | MUSA 平台的名字字符串(以其分支源码为准) |
| platformProperties 属性面 | 沿用 CUDA/HIP 同款属性集(官方文档确认 HIP 与 CUDA 属性相同) |
| supportsKernels 空集 | 逐内核实现清单(非键/积分/PME……) |
| contextCreated 打印 | 设备初始化(对照 GROMACS DeviceStreamManager 的角色) |
| registerKernelFactory(“*”) | 按内核族分组的工厂注册 |
三、常见报错与排查
问题 1:现象——loadPluginsFromDirectory返回了文件名列表,但getNumPlatforms()里找不到新平台。
根因:动态库加载成功 ≠ 注册成功。三种典型:插件 .so 编译时链接的 OpenMM 库与运行时版本不匹配(符号版本错位,加载器吞了错误);registerPlatforms 里 new 平台后没调 registerPlatform;C++ 符号被名字修饰(忘了 extern “C”),加载器按 C 符号找不到入口。
解法:用nm -D ProbePlatform.so | grep registerPlatforms检查导出符号是未修饰的 C 名;确认链接的 OpenMM 与运行时一致;在 registerPlatforms 里加 printf(本文骨架的做法)直接观察是否被调用。
问题 2:现象——插件加载时报 undefined symbol(如 KernelFactory 的 vtable)。
根因:插件与 OpenMM 主库用不同编译器/ABI(如插件用 clang++ 编、OpenMM 是 g++ 编的发行包);或 C++ 标准库版本不一致。
解法:与 OpenMM 构建用同一工具链编译插件(最稳做法:用 OpenMM 源码树自带的 CMake 配置);检查 CMAKE_CXX_STANDARD 与发行包要求一致。
问题 3:现象——Context 创建时报 “Platform Probe does not support kernel IntegrateVerletStep”(或类似内核名)。
根因:这不是 bug,是 1.3 节的宽容性设计在正常工作——你的平台没实现该内核,而这台模拟需要它。supportsKernels 返回 false / 工厂抛异常,两处任一都会走到这个报错。
解法:按模拟报错里点名的内核逐个实现(报错信息就是你的待办清单);或者给体系换一个已支持的要素组合(如先用 CPU 平台跑通,平台补齐后再切回)。
问题 4:现象——自定义力(CustomForce)模拟在自己的平台上跑不起来,报内核不支持。
根因:传统架构下 CustomForce 的内核每平台一份,新平台没写就跑不了。
解法:OpenMM 8.1.0+ 用CustomCPPForceImpl写自定义力——平台无关的 C++ 实现,任何平台(包括你的国产平台)都能跑(CPU 计算该力,不占 GPU);8.5.0 还可考虑 PythonForce。这是渐进式移植的功能兜底路径。
四、动手练习
练习 1(基础):编译并加载本文骨架插件,跑通 Python 测试脚本。
判定成功标准:平台清单含 “Probe”;getPlatformByName('Probe')不抛异常;属性面含 ProbePrecision。
练习 2(进阶):给 ProbePlatform 的 supportsKernels 实现真实的集合判断(维护std::set<std::string> supportedKernels),并让它声明支持一个假内核名 “MyDemoKernel”;然后写 Python 验证Platform.findPlatform(["MyDemoKernel"])能返回 Probe 平台。
判定成功标准:findPlatform 返回的平台的 getName() 是 “Probe”;请求一个未声明内核(如 [“IntegrateVerletStep”])时不会匹配到 Probe。
练习 3(思考题,无标准答案):OpenMM 的“先全部 registerPlatforms 再 registerKernelFactories”时序与 GROMACS 的“编译期单后端”各自适合什么样的生态演化?思考方向(验证要点):① AMD HIP 从插件到主线的路径说明了什么准入门槛;② 摩尔线程同时维护 GROMACS fork 与 openmm-musa 插件,哪个更容易跟随上游升级(对照其 master 与上游 0 commits ahead 的镜像策略);③ “跨插件补内核”机制对第三方生态(如自定义力库)的意义。
五、小结与下一篇预告
本篇补全了 OpenMM 侧的工程地基:插件=动态库+两个 C 导出函数,加载时序“先平台后内核工厂”支撑跨插件组合;新建平台四件套(Platform/KernelFactory/KernelImpl/注册)里不实现内核只是“不支持该模拟”而非加载失败——渐进式移植的机制基础;CustomCPPForceImpl(8.1+)给自定义力提供平台无关兜底。骨架插件的每一步(编译/加载/注册/查找)都有独立验证点。
下一篇进入本系列的重头戏:摩尔线程 MUSA 全栈移植——musify 双轨法(直转+手改)、warp 32→128 的六处连锁修改、MUSA SCS 容器的完整实操。第 5 篇的抽象层地图和本篇的插件协议都将在那里兑现价值。
本篇认知问题回显(FAQ)
Q1:OpenMM 平台插件需要实现哪些导出函数?加载顺序是什么?
A:必须实现 PluginInitializer.h 声明的两个 C 导出函数:extern "C" void registerPlatforms()(创建并注册 Platform)与extern "C" void registerKernelFactories()(注册内核工厂)。加载器对目录内所有插件先逐个调用 registerPlatforms、全部完成后再逐个调用 registerKernelFactories——这个时序允许插件 B 给插件 A 注册的平台补充内核。
Q2:OpenMM 新建一个平台要写哪几类代码?不实现某个内核会怎样?
A:四件套:Platform 子类(名称/属性/支持的内核声明)、KernelFactory 子类(内核生产工厂)、每个内核的 KernelImpl 子类(实现)、注册代码(registerPlatforms 里调 Platform::registerPlatform)。不实现的内核只是让该平台无法运行需要该内核的模拟(创建 Context 时抛异常),不影响插件加载——支持渐进式实现。
Q3:OpenMM 插件放在哪个目录?怎么加载?
A:默认放在 OpenMM 安装目录的 lib/plugins,或用环境变量 OPENMM_PLUGIN_DIR 指定;C++ 用Platform::loadPluginsFromDirectory(Platform::getDefaultPluginsDirectory())批量加载或loadPluginLibrary()单独加载,Python 侧是同名 static 方法。
Q4:OpenMM 8.1 的 CustomCPPForceImpl 对新平台适配有什么用?
A:它是平台无关的 C++ 层自定义力实现基础设施——自定义力用 CustomCPPForceImpl 写一份就能在任何平台(包括尚未实现该力 GPU 内核的新国产平台)上运行(CPU 计算该力、结果回传),代价是该力不享受 GPU 加速;这让新平台可以“先全功能跑通、再逐步内核 GPU 化”。