分子模拟异构算力适配开发教程(20):完整项目——异构算力 MD 平台:适配层、调度器、基准流水线三位一体
分子模拟异构算力适配开发教程(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__ import annotations
import json
import sys
import time
from pathlib import Path
# ── 组件复用(本系列产出,非重写)──────────────────────────────────
# 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 连接真实模块)———
class Platform:
"""平台的组装点:三个组件 + 一个档案口。"""
def __init__(self, slots: int = 2, archive: str = "platform-ledger.csv"):
from md_scheduler import MDScheduler
self.scheduler = MDScheduler(slots=slots, mode="demo")
# 能力矩阵带缓存(1.2 节决策:探测有成本)
self._caps_cache: list[dict] | None = None
self._caps_ts: float = 0.0
self.archive = Path(archive)
def capabilities(self, ttl: float = 300.0) -> list[dict]:
"""能力矩阵:TTL 缓存——5 分钟内复用探测结果。"""
if self._caps_cache is None or time.time() - self._caps_ts > ttl:
from adapter_layer import probe_gromacs, probe_openmm
self._caps_cache = [probe_gromacs(), probe_openmm()]
self._caps_ts = time.time()
return self._caps_cache
def submit_batch(self, specs: list[dict]) -> dict:
"""批量提交:MDJobSpec 字典 → 协商验证 → 调度器队列。
注意顺序:先协商(提交时就说不,而不是运行时撞墙——18 篇问题 4)。"""
from adapter_layer import MDJobSpec, negotiate
from md_scheduler import MDJob
accepted = []
for s in specs:
spec = MDJobSpec(**s)
chosen = negotiate(spec, self.capabilities()) # 提交期协商
accepted.append(MDJob(
job_id=f"{spec.kind}-{int(time.time()*1000)%100000}",
tpr=spec.tpr or "-", nsteps=spec.nsteps or 10,
priority=spec.backend_pref == "interactive" and 5 or 0))
self.scheduler.jobs = accepted
stats = self.scheduler.run()
# 档案口(12/16 篇):结果追加进 ledger——平台的记忆
with self.archive.open("a") as f:
f.write(f"{time.strftime('%FT%T')},{json.dumps(stats)}\n")
return stats
def status(self) -> dict:
"""队列/槽位快照(1.3 节指标的来源)。"""
return {"capabilities": self.capabilities(),
"jobs": [{"id": j.job_id, "state": j.state.value}
for j in self.scheduler.jobs]}
def main() -> None:
cmd = sys.argv[1] if len(sys.argv) > 1 else "status"
plat = Platform(slots=2)
if cmd == "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))
elif cmd == "status":
print(json.dumps(plat.status(), ensure_ascii=False, indent=2))
elif cmd == "reconcile":
from reconcile import load_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,多租户弹性目标形态);演进平滑的关键是适配层与调度内核对底座零感知(底座差异被环境注入与提交翻译器吸收)。
更多推荐



所有评论(0)