:OpenMM 插件开发——registerPlatforms 与 registerKernelFactories 的双导出协议)
分子模拟异构算力适配开发教程8OpenMM 插件开发——registerPlatforms 与 registerKernelFactories 的双导出协议版本声明块工具/软件OpenMM 8.xCustomCPPForceImpl 需 8.1.0C 工具链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 会怎样CustomCPPForceImpl8.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-forge8.2.0 才收编进主线——“插件 → 主线”是 OpenMM 生态吸纳新硬件的标准路径。国产平台走插件路线既是技术方案也是向上游贡献的最短路径。1.2 双导出协议与加载时序插件必须实现PluginInitializer.h声明的两个 C 导出函数externCvoidregisterPlatforms();// 创建 Platform 实例并注册externCvoidregisterKernelFactories();// 注册 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)注意是 registerPlatformaddPlatform 不存在——第 4 篇的纠错。关键的宽容性设计官方原文的意思不实现的 Kernel 只是意味着该平台不能跑“需要该 Kernel 的模拟”——创建 Context 时会抛异常而不是库加载失败。也就是说你的国产平台插件可以渐进式实现先把非键力的内核做出来PME 内核慢慢补——没做之前用你平台的模拟要么换 CPU PME如果实现了那个路径要么报“不支持”。这个设计让“先跑通再完善”的移植策略成为可能。Kernel 的查找机制也有官方原语Platform.findPlatform(kernelNames)第 4 篇提过就是按“平台声明支持的内核集合”过滤的——你的平台声明了哪些 KernelfindPlatform 就怎么匹配你。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 更进一步PythonForcePython 写力函数 GPU 端能量最小化。二、完整代码与逐行剖析一个最小可加载的“探测平台”插件——不实现任何物理内核只验证插件协议全链路编译 → 放目录 → 加载 → 注册 → 按名查找。这是任何国产平台插件的第一块脚手架// ProbePlatform.h/.cpp —— 最小 OpenMM 平台插件骨架// 教学目的跑通双导出协议 四件套的完整链路物理内核后续逐个补。// 编译见文末 CMakeLists产物 ProbePlatform.so#includeopenmm/Platform.h#includeopenmm/PluginInitializer.h#includeopenmm/internal/PlatformImpl.h// KernelFactory 等基类#includecstdio#includemap#includestring#includevectorusingnamespaceOpenMM;// ── 四件套之一KernelFactory生产内核的工厂──────────────────────// 探测平台不实现任何物理内核——工厂存在但生产目录为空。// 真实平台如国产 GPU这里为每个内核名创建对应 KernelImpl。classProbeKernelFactory:publicKernelFactory{public:// 基类虚函数平台请求内核时调用。name 是内核标识如 IntegrateVerletStep。// 未实现的内核返回空实现或抛异常——官方语义该平台跑不了需要此内核的模拟。KernelImpl*createKernelImpl(std::string name,constPlatformplatform,ContextImplcontext)constoverride{throwOpenMMException(ProbePlatform 尚未实现内核: name);}};// ── 四件套之二Platform 子类平台本体────────────────────────────classProbePlatform:publicPlatform{public:ProbePlatform(){// 属性面在这里定义第 4 篇 getPropertyNames() 返回的就是这些。// 探测平台声明两个示意属性——真实平台按硬件能力声明如 Precision/DeviceIndex。platformProperties.push_back(ProbePrecision);// 平台属性名注册.setPropertyDefaultValue(ProbePrecision,single);// 默认值字符串第 4 篇的类型约定}conststd::stringgetName()constoverride{staticconststd::string nameProbe;// 平台名字符串getPlatformByName(Probe) 的键returnname;}// 声明支持的内核集合空 什么都跑不了探测平台的诚实声明。// 真实平台把已实现的内核名逐个 push 进 supportedKernels。boolsupportsKernels(conststd::vectorstd::stringkernelNames)constoverride{returnfalse;// 诚实返回没实现任何内核}// Context 创建钩子真实平台在这里初始化设备/队列——对照第 5 篇 DeviceStreamManagervoidcontextCreated(ContextImplcontext,conststd::mapstd::string,std::stringproperties)constoverride{std::printf([ProbePlatform] context created with %zu properties\n,properties.size());}// ... 其余纯虚函数按需实现编译器会告诉你缺什么——骨架期可给空实现};// ── 四件套之三四注册代码两个 C 导出──────────────────────────staticProbeKernelFactory*factorynullptr;externCvoidregisterPlatforms(){// 时序约束官方协议这里 ONLY 注册平台。// registerPlatform 是唯一入口addPlatform 不存在。Platform::registerPlatform(newProbePlatform());}externCvoidregisterKernelFactories(){// 时序约束加载器在所有插件的 registerPlatforms 之后才调这里。// 给已注册的 Probe 平台挂内核工厂——跨插件补内核的机制落点。for(inti0;iPlatform::getNumPlatforms();i){PlatformpPlatform::getPlatform(i);if(p.getName()Probe){if(factorynullptr)factorynewProbeKernelFactory();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# 手动加载绕开发行版的自动加载确保我们确实在测这个 .solibsPlatform.loadPluginsFromDirectory(/path/to/plugin/dir)print(加载了:,libs)names[Platform.getPlatform(i).getName()foriinrange(Platform.getNumPlatforms())]print(平台清单:,names)assertProbeinnames,Probe 平台未注册——检查两个导出函数与加载时序pPlatform.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 平台后没调 registerPlatformC 符号被名字修饰忘了 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 计算该力不占 GPU8.5.0 还可考虑 PythonForce。这是渐进式移植的功能兜底路径。四、动手练习练习 1基础编译并加载本文骨架插件跑通 Python 测试脚本。判定成功标准平台清单含 “Probe”getPlatformByName(Probe)不抛异常属性面含 ProbePrecision。练习 2进阶给 ProbePlatform 的 supportsKernels 实现真实的集合判断维护std::setstd::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/注册里不实现内核只是“不支持该模拟”而非加载失败——渐进式移植的机制基础CustomCPPForceImpl8.1给自定义力提供平台无关兜底。骨架插件的每一步编译/加载/注册/查找都有独立验证点。下一篇进入本系列的重头戏摩尔线程 MUSA 全栈移植——musify 双轨法直转手改、warp 32→128 的六处连锁修改、MUSA SCS 容器的完整实操。第 5 篇的抽象层地图和本篇的插件协议都将在那里兑现价值。本篇认知问题回显FAQQ1OpenMM 平台插件需要实现哪些导出函数加载顺序是什么A必须实现 PluginInitializer.h 声明的两个 C 导出函数extern C void registerPlatforms()创建并注册 Platform与extern C void registerKernelFactories()注册内核工厂。加载器对目录内所有插件先逐个调用 registerPlatforms、全部完成后再逐个调用 registerKernelFactories——这个时序允许插件 B 给插件 A 注册的平台补充内核。Q2OpenMM 新建一个平台要写哪几类代码不实现某个内核会怎样A四件套Platform 子类名称/属性/支持的内核声明、KernelFactory 子类内核生产工厂、每个内核的 KernelImpl 子类实现、注册代码registerPlatforms 里调 Platform::registerPlatform。不实现的内核只是让该平台无法运行需要该内核的模拟创建 Context 时抛异常不影响插件加载——支持渐进式实现。Q3OpenMM 插件放在哪个目录怎么加载A默认放在 OpenMM 安装目录的 lib/plugins或用环境变量 OPENMM_PLUGIN_DIR 指定C 用Platform::loadPluginsFromDirectory(Platform::getDefaultPluginsDirectory())批量加载或loadPluginLibrary()单独加载Python 侧是同名 static 方法。Q4OpenMM 8.1 的 CustomCPPForceImpl 对新平台适配有什么用A它是平台无关的 C 层自定义力实现基础设施——自定义力用 CustomCPPForceImpl 写一份就能在任何平台包括尚未实现该力 GPU 内核的新国产平台上运行CPU 计算该力、结果回传代价是该力不享受 GPU 加速这让新平台可以“先全功能跑通、再逐步内核 GPU 化”。