分子模拟异构算力适配开发教程(20):完整项目——异构算力 MD 平台:适配层、调度器、基准流水线三位一体
版本声明块
- 工具/软件:本系列全部组件(适配层 18 篇、调度器 17 篇、流水线 12 篇、对账器 19 篇);底座 K8s+Volcano/HAMi(14/15 篇)或 Slurm+Apptainer(16 篇)
- 语言/环境:Python 3.10+、Linux
- 本文目标:读完你拥有一个可落地的平台蓝图——分层架构、目录骨架、核心代码组装、可观测性设计与部署决策,能把本系列任意单篇的产出接入对应层
一句话结论:异构 MD 平台 =四层总装——底座层(K8s+Volcano/HAMi 或 Slurm+Apptainer,管设备与作业组)、适配层(18 篇:探测/协商/注入/执行/归一)、调度层(17 篇:槽位回收/补位/aging 的 continuous batching 内核)、运维层(12 篇基准档案 + 19 篇对账巡检 + Prometheus 指标)——用户只见统一的 MDJobSpec 提交接口,异构性(引擎×硬件×精度×调度器)全部被中间两层吸收。
〇、本篇要解决的认知问题
- 平台的完整分层是什么?每层对应本系列哪几篇的产出?
- 三大组件(适配层/调度器/流水线)怎么组装——接口与数据流?
- 可观测性(指标/档案/巡检)怎么设计成平台的“体检系统”?
- 部署形态怎么选(K8s 原生 / Slurm 旁挂 / 单机起步)?
一、机制解析
1.1 平台分层:19 篇知识的归位图
为什么这一节对你重要:总装不是把 19 篇代码堆一起——每篇的产出有它的层,放错层(比如把引擎协商塞进 K8s 调度器、或把槽位管理下沉到 gmx 命令行)会造成职责纠缠,这是平台工程最常见的返工源。
┌────────────────────────────────────────────────────────────┐ │ 用户接口层 mdctl submit / REST API / Web │ │ 统一作业描述 MDJobSpec(18 篇) │ ├────────────────────────────────────────────────────────────┤ │ 调度层 MDScheduler(17 篇:槽位回收/补位/aging) │ │ + 平台队列策略(优先级/配额的 app 层版本) │ ├────────────────────────────────────────────────────────────┤ │ 适配层 Adapter 族(18 篇:探测/协商/注入/执行/归一) │ │ GromacsAdapter / OpenMMAdapter / ... │ ├────────────────────────────────────────────────────────────┤ │ 底座层 K8s + Volcano/HAMi(14/15 篇) │ │ 或 Slurm + GRES + Apptainer(16 篇) │ │ (设备分配、可见性注入、gang/配额) │ └────────────────────────────────────────────────────────────┘ 旁路系统(贯穿四层): 基准流水线(12 篇 perf-archive.json)→ 性能档案库 对账巡检(19 篇 reconcile)→ 数值健康 诊断体系(19 篇分类学)→ 故障定位各层职责的一句话边界:底座管“有什么资源、怎么隔离”;适配层管“这个作业具体怎么跑”;调度层管“什么时候、在哪个槽位跑”;用户层管“用户说什么语言”。旁路系统不属于任何层——它们消费所有层的输出。
1.2 数据流:一次作业的一生
① 用户提交 MDJobSpec(kind=tpr, precision=mixed, priority=5) ② 调度层入队(有效优先级 = 5 + 等待×aging——17 篇) ③ 槽位空闲 → 取出作业 → 交给适配层 ④ 适配层探测(缓存的能力矩阵:gmx 三要素 + OpenMM 平台表——18 篇) → 协商(形态/精度/偏好过滤)→ 选定 GromacsAdapter ⑤ 环境注入(build_env:device → CUDA_VISIBLE_DEVICES——18 篇) → 执行(gmx mdrun 全家桶 + -cpi 断点语义——3/16 篇) ⑥ 运行中:底座的健康流(device plugin Unhealthy——14 篇)异常 → 调度层标记、回收、换槽位续跑(17 篇 + checkpoint) ⑦ 完成:JobResult(ns/day、环境快照、轨迹路径) → 归一化入档(perf-ledger.csv 追加——16 篇) ⑧ 夜间巡检:抽体系跑三方对账(19 篇)→ 数值健康分两个设计决策值得点名:能力矩阵带缓存(探测有成本——gmx --version 是进程调用、OpenMM 枚举要 import;缓存 + TTL + 手动刷新按钮);健康流是旁挂的(调度器不主动轮询设备,订阅底座的事件——掉卡秒级感知的 K8s 版是 ListAndWatch,Slurm 版是作业自检段的退出码)。
1.3 可观测性:平台的体检系统
三个维度、三套数据、一个消费口:
| 维度 | 数据源 | 存储 | 消费 |
|---|---|---|---|
| 性能 | md.log 六指标(12 篇流水线) | perf-archive.json / ledger.csv | 趋势图、瓶颈定位(19 篇)、铁律 9 实测依据 |
| 数值 | 对账巡检(19 篇) | 巡检报告 | 版本升级的回归门禁 |
| 运行 | 作业状态、槽位利用率、队列深度 | Prometheus 指标 | 告警、容量规划 |
Prometheus 指标命名(平台侧约定,Prom 命名惯例<namespace>_<subsystem>_<name>):
mdplatform_jobs_total{engine,backend,precision,state} # 计数:作业按维度 mdplatform_slots{device_type} # gauge:槽位在用/空闲 mdplatform_job_ns_per_day{engine,backend,system} # histogram:性能分布 mdplatform_reconcile_pass_ratio{pair} # gauge:对账通过率 mdplatform_queue_wait_seconds{priority_class} # histogram:排队延迟(SLA)设计要点:指标标签维度对齐能力矩阵(engine/backend/precision)——性能问题定位时能下钻到“是不是 MUSA 后端的作业普遍慢”;对账通过率进指标——数值健康从“某天有人发现”变成“仪表盘红绿灯”。
1.4 部署形态:三种起步
形态 A:单机旁挂(最快起步)——适配层 + 调度器跑在一台 GPU 服务器(cron 或 systemd),无 K8s/Slurm 依赖。适合课题组级(几人到十几人、几张卡)。本篇骨架代码就是这个形态的直接可用版。
形态 B:Slurm 旁挂(HPC 存量)——平台调度器翻译作业为 sbatch 脚本(16 篇模板),GRES/可见性交给 Slurm,平台只管队列策略与适配层。适合算力中心已有 Slurm 且不想动底座。
形态 C:K8s 原生(平台化目标)——调度层对接 Volcano Queue/PodGroup(15 篇)、HAMi 管切分(14 篇),适配层以 Job/DaemonSet 形态部署。多租户、弹性、混载(推理+MD)的目标形态。
演进路径 A→B/C 平滑:因为适配层与调度内核在三种形态里零改动(它们不感知底座,底座差异被 build_env 与“提交翻译器”吸收)——这是 18 篇分层设计的回报。
二、完整代码与逐行剖析
平台骨架(把 17/18/19 篇组件组装成可运行的最小平台——形态 A 直接可用):
#!/usr/bin/env python3"""mdplatform.py —— 异构 MD 平台最小可用版(形态 A:单机)。 组装:MDScheduler(17)+ Adapter 族与协商(18)+ 档案与对账(12/19)。 运行:python mdplatform.py submit jobs.json # 提交一批作业 python mdplatform.py status # 查看队列/槽位 python mdplatform.py reconcile a b # 对账两个作业的能量 """from__future__importannotationsimportjsonimportsysimporttimefrompathlibimportPath# ── 组件复用(本系列产出,非重写)──────────────────────────────────# 17 篇:调度内核(槽位回收/补位/aging)# from md_scheduler import MDScheduler, MDJob as SchedJob, State# 18 篇:适配层(探测/协商/注入/执行)# from adapter_layer import (MDJobSpec, probe_gromacs, probe_openmm,# negotiate, GromacsAdapter, OpenMMAdapter)# 19 篇:对账器# from reconcile import load_energies, reconcile_L1, Tolerance# —— 教学版内联演示组装关系(生产用上面 import 连接真实模块)———classPlatform:"""平台的组装点:三个组件 + 一个档案口。"""def__init__(self,slots:int=2,archive:str="platform-ledger.csv"):frommd_schedulerimportMDScheduler self.scheduler=MDScheduler(slots=slots,mode="demo")# 能力矩阵带缓存(1.2 节决策:探测有成本)self._caps_cache:list[dict]|None=Noneself._caps_ts:float=0.0self.archive=Path(archive)defcapabilities(self,ttl:float=300.0)->list[dict]:"""能力矩阵:TTL 缓存——5 分钟内复用探测结果。"""ifself._caps_cacheisNoneortime.time()-self._caps_ts>ttl:fromadapter_layerimportprobe_gromacs,probe_openmm self._caps_cache=[probe_gromacs(),probe_openmm()]self._caps_ts=time.time()returnself._caps_cachedefsubmit_batch(self,specs:list[dict])->dict:"""批量提交:MDJobSpec 字典 → 协商验证 → 调度器队列。 注意顺序:先协商(提交时就说不,而不是运行时撞墙——18 篇问题 4)。"""fromadapter_layerimportMDJobSpec,negotiatefrommd_schedulerimportMDJob accepted=[]forsinspecs:spec=MDJobSpec(**s)chosen=negotiate(spec,self.capabilities())# 提交期协商accepted.append(MDJob(job_id=f"{spec.kind}-{int(time.time()*1000)%100000}",tpr=spec.tpror"-",nsteps=spec.nstepsor10,priority=spec.backend_pref=="interactive"and5or0))self.scheduler.jobs=accepted stats=self.scheduler.run()# 档案口(12/16 篇):结果追加进 ledger——平台的记忆withself.archive.open("a")asf:f.write(f"{time.strftime('%FT%T')},{json.dumps(stats)}\n")returnstatsdefstatus(self)->dict:"""队列/槽位快照(1.3 节指标的来源)。"""return{"capabilities":self.capabilities(),"jobs":[{"id":j.job_id,"state":j.state.value}forjinself.scheduler.jobs]}defmain()->None:cmd=sys.argv[1]iflen(sys.argv)>1else"status"plat=Platform(slots=2)ifcmd=="submit":specs=json.loads(Path(sys.argv[2]).read_text(encoding="utf-8"))print(json.dumps(plat.submit_batch(specs),ensure_ascii=False,indent=2))elifcmd=="status":print(json.dumps(plat.status(),ensure_ascii=False,indent=2))elifcmd=="reconcile":fromreconcileimportload_energies,reconcile_L1,Tolerance r=reconcile_L1(load_energies(Path(sys.argv[2])),load_energies(Path(sys.argv[3])),Tolerance())print(json.dumps(r,ensure_ascii=False,indent=2))else:print(__doc__)if__name__=="__main__":main()配套的作业描述文件(用户视角的全部复杂度——这就是“用户只说什么语言”的答案):
// jobs.json —— 用户提交的全部语言:一个 JSON 数组[{"kind":"tpr","tpr":"/data/benchMEM.tpr","nsteps":5000,"precision":"mixed","backend_pref":"gromacs","device":0},{"kind":"tpr","tpr":"/data/ligand-prod.tpr","nsteps":500000,"precision":"mixed","backend_pref":null,"platform_hint":null,"device":1}]逐段剖析:
- 组装的本质是“连接件”:Platform 类没有重写任何引擎逻辑——它做的是三件事:能力缓存(性能决策)、提交期协商(体验决策:错误早爆)、档案口(记忆决策)。平台代码的价值在连接件,不在重复造轮子——19 篇的组件各就各位。
submit_batch的协商前置(第 18 篇问题 4 的落实):作业不合法(如 tpr+double 但 gmx 是 mixed 构建)在提交秒级被拒——带原因的拒绝比运行三小时后的崩溃好一百倍。status()的输出结构直接映射 1.3 节指标:capabilities 是标签维度来源、jobs 的 state 分布是队列深度——接 Prometheus exporter 就是把这两个 dict 变成 gauge/counter 的事。- jobs.json 里
backend_pref: null(自动协商)与显式gromacs并存——平台对“懂的用户”开放控制、对“不懂的用户”提供默认——两种用户都被同一层服务。
部署清单(形态 C 的 K8s 化要点,对照 14/15 篇):
# 平台组件的 K8s 形态(示意):# md-scheduler → Deployment(无状态,对接 Volcano Queue 的 app 层策略)# adapter-worker → Job/DaemonSet(有状态执行,走 HAMi vGPU 申请 gpumem)# capability-probe → CronJob(定期刷新能力矩阵——底座设备变化的感知)# prometheus exporter → 侧车容器(消费 status() 输出)# 底座前置:Volcano 部署 + HAMi 部署 + 各厂商 device plugin#(全部在第 14/15 篇有落地步骤;平台的 K8s 化不改变适配层与调度内核——1.4 节承诺)三、常见报错与排查
问题 1:现象——平台上线后用户反馈“还是直接 gmx 命令快,平台有开销”。
根因:开销解剖——正常开销(协商毫秒级、环境构造微秒级)可忽略;异常开销三个来源:能力探测无缓存每次全量探测(18 篇决策未落地);调度器 poll 过密(17 篇问题 3);作业排队时间被计入“平台慢”(其实是槽位不足的容量问题)。
解法:三层分别处理——TTL 缓存(本文 capabilities);poll 调到秒级;容量问题看 queue_wait_seconds 指标(1.3 节)说话,加槽位或错峰。用指标分清“开销”与“排队”——多数抱怨其实是后者。
问题 2:现象——某引擎升级后平台作业全体数值告警(对账巡检红了)。
根因:这是巡检系统正常工作——引擎升级改变了数值行为(精度路径/积分器实现变化),平台用 19 篇的对账把它拦在了生产之前。
解法:按 19 篇层级定性差异(合法变化 vs 真 bug)——合法则在平台登记新基线(巡检的黄金值随版本更新,带版本标签);真 bug 则冻结该引擎版本(能力矩阵把它摘掉),上游修复后再放行。巡检不是挡板的成本,是灰度的依据。
问题 3:现象——双底座(K8s + Slurm 各管一部分节点)时,能力矩阵与可见性行为不一致。
根因:两底座注入语义差异(Slurm 的 job step 环境 vs K8s 的 Allocate 注入)漏过了适配层的统一收口——某处代码绕过 build_env 直接用了 os.environ。
解法:架构纪律检查——适配层外的任何代码不许构造子进程环境(grep 检查 subprocess 调用点的 env= 参数);能力探测在双底座节点各自跑(CronJob/旁挂探测按节点记录)——矩阵的粒度是节点不是集群。
问题 4:现象——平台故障(调度器崩了)时在跑的作业全部丢失。
根因:把“调度器存活”与“作业存活”耦合了——其实 gmx/OpenMM 进程是底座(systemd/K8s/Slurm)的公民,调度器崩不该带走它们;丢失的是“管理”不是“计算”。
解法:调度器崩溃恢复流程——重启后扫描作业产物目录(md.log/deffnm 文件在)、按 checkpoint 状态重建作业清单(-cpi 语义天然支持)、继续调度;本文 Platform 的 jobs 状态可持久化到 ledger(已经在做)——控制面与数据面分离,这是 17 篇“调度内核与执行后端分离”的运维版。
四、动手练习
练习 1(基础):把 17/18 篇的模块文件(md_scheduler.py/adapter_layer.py)与本文 mdplatform.py 放同目录,跑python mdplatform.py status,看到带缓存的能力矩阵输出。
判定成功标准:status 输出双引擎能力(或明确的 unavailable);二次调用 status 在 TTL 内不再触发探测(加个 print 在 probe 里验证缓存生效)。
练习 2(进阶):给 Platform 加bench命令——调第 12 篇流水线对当前槽位跑 benchMEM 短程基准,把 ns/day 追加进 ledger 并打印与历史均值(读 ledger 计算)的对比。
判定成功标准:ledger 里出现新的性能记录;对比输出含“当前/历史均值/偏差%”三要素;偏差超过 30% 时给出 19 篇决策树的入口提示。
练习 3(思考题,无标准答案):平台上线一年后,能力矩阵膨胀到几十个条目(多引擎版本 × 多硬件 × 多精度),协商变慢、维护变难——该怎么治理?思考方向(验证要点):① 矩阵的生命周期管理(废弃条目的淘汰机制——性能档案能否提供依据);② 协商结果的缓存与失效策略;③ “能力即代码”(capability as config)还是“能力即探测”(runtime probing)的长期取舍。
五、系列总结
20 篇收官。回到第 1 篇的三个问题,现在的你能这样回答:
- 修改引擎:GROMACS 四后端的构建(2 篇)与执行控制(3 篇)、gpu_utils 抽象层(5 篇)、HIP/SYCL/MUSA 三条移植路线的完整方法论(6/7/9 篇)——昇腾的特殊性(11 篇)与“没有后端”时的决策树。
- 封装引擎:OpenMM 平台抽象与插件协议(4/8/10 篇)、双引擎统一适配层(18 篇)——能力探测、协商、环境收口。
- 调度封装:从 Device Plugin/vGPU(14 篇)到 Volcano/vNPU(15 篇)到 Slurm/Apptainer(16 篇)的底座全景,推理调度思想迁移的 MD 批调度器(17 篇),验证与诊断闭环(12/19 篇),最终总装成平台(20 篇)。
贯穿全系列的十条铁律(第 0 篇计划定义)在每篇各自主战场兑现:版本先锚定(2/13 篇的环境变量三代史)、单后端编译(2 篇)、工具边界(6 篇 hipify)、昇腾无后端的事实链(11 篇)、GPU-resident 前提(3 篇)、性能带上下文(12 篇)、双精度慎用(2 篇)、移植必须过验证(12 篇)、切分必须实测(14/17 篇)、封装不改物理(18/19 篇)。
下一步去哪:把第 9 篇的 SCS 容器跑起来,用第 12 篇流水线给你的硬件建档,第 18 篇的适配层接入你手头的引擎——平台从第一个真实作业开始生长。
本篇认知问题回显(FAQ)
Q1:异构 MD 平台的完整分层是什么?
A:四层——用户接口层(统一 MDJobSpec 提交)、调度层(continuous batching 内核:槽位回收/补位/aging)、适配层(能力探测/后端协商/环境注入/执行/结果归一)、底座层(K8s+Volcano/HAMi 或 Slurm+GRES+Apptainer,管设备分配与可见性);旁路系统(基准档案、对账巡检、诊断体系)贯穿四层——每层职责一句话:底座管资源隔离、适配层管作业怎么跑、调度层管何时何槽位、用户层管语言。
Q2:平台的三大组件怎么组装?
A:连接件模式不重复造轮子:Platform 类做三件事——能力矩阵 TTL 缓存(探测有成本)、提交期协商前置(作业不合法秒级拒绝而非运行数小时后崩溃)、档案口(结果追加 ledger 形成平台记忆);调度器崩不影响在跑作业(控制面与数据面分离,重启后按 checkpoint 产物重建清单续调)。
Q3:MD 平台需要哪些可观测性指标?
A:三维度对齐能力矩阵标签(engine/backend/precision):性能维度(mdplatform_job_ns_per_day histogram、六指标来自 12 篇流水线)、数值维度(mdplatform_reconcile_pass_ratio 对账通过率——版本升级的回归门禁)、运行维度(jobs_total 计数、slots gauge、queue_wait_seconds 排队延迟 SLA)——性能定位能下钻到“某后端是否普遍慢”,数值健康从人工发现变成仪表盘红绿灯。
Q4:平台部署形态怎么选?
A:三种起步——单机旁挂(适配层+调度器 systemd 跑一台 GPU 服务器,课题组级最快可用)、Slurm 旁挂(翻译作业为 sbatch,GRES 交给 Slurm,HPC 存量友好)、K8s 原生(对接 Volcano Queue/HAMi vGPU,多租户弹性目标形态);演进平滑的关键是适配层与调度内核对底座零感知(底座差异被环境注入与提交翻译器吸收)。